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(store) = ThreadMetadataStore::try_global(cx) {
2080            store.update(cx, |store, cx| store.unarchive(&session_id, cx));
2081        }
2082
2083        if let Some(conversation_view) = self.background_threads.remove(&session_id) {
2084            self.set_active_view(
2085                ActiveView::AgentThread { conversation_view },
2086                focus,
2087                window,
2088                cx,
2089            );
2090            return;
2091        }
2092
2093        if let ActiveView::AgentThread { conversation_view } = &self.active_view {
2094            if conversation_view
2095                .read(cx)
2096                .active_thread()
2097                .map(|t| t.read(cx).id.clone())
2098                == Some(session_id.clone())
2099            {
2100                cx.emit(AgentPanelEvent::ActiveViewChanged);
2101                return;
2102            }
2103        }
2104
2105        if let Some(ActiveView::AgentThread { conversation_view }) = &self.previous_view {
2106            if conversation_view
2107                .read(cx)
2108                .active_thread()
2109                .map(|t| t.read(cx).id.clone())
2110                == Some(session_id.clone())
2111            {
2112                let view = self.previous_view.take().unwrap();
2113                self.set_active_view(view, focus, window, cx);
2114                return;
2115            }
2116        }
2117
2118        self.external_thread(
2119            Some(agent),
2120            Some(session_id),
2121            work_dirs,
2122            title,
2123            None,
2124            focus,
2125            window,
2126            cx,
2127        );
2128    }
2129
2130    pub(crate) fn create_agent_thread(
2131        &mut self,
2132        server: Rc<dyn AgentServer>,
2133        resume_session_id: Option<acp::SessionId>,
2134        work_dirs: Option<PathList>,
2135        title: Option<SharedString>,
2136        initial_content: Option<AgentInitialContent>,
2137        workspace: WeakEntity<Workspace>,
2138        project: Entity<Project>,
2139        agent: Agent,
2140        focus: bool,
2141        window: &mut Window,
2142        cx: &mut Context<Self>,
2143    ) {
2144        if self.selected_agent != agent {
2145            self.selected_agent = agent.clone();
2146            self.serialize(cx);
2147        }
2148
2149        cx.background_spawn({
2150            let kvp = KeyValueStore::global(cx);
2151            let agent = agent.clone();
2152            async move {
2153                write_global_last_used_agent(kvp, agent).await;
2154            }
2155        })
2156        .detach();
2157
2158        let thread_store = server
2159            .clone()
2160            .downcast::<agent::NativeAgentServer>()
2161            .is_some()
2162            .then(|| self.thread_store.clone());
2163
2164        let connection_store = self.connection_store.clone();
2165
2166        let conversation_view = cx.new(|cx| {
2167            crate::ConversationView::new(
2168                server,
2169                connection_store,
2170                agent,
2171                resume_session_id,
2172                work_dirs,
2173                title,
2174                initial_content,
2175                workspace.clone(),
2176                project,
2177                thread_store,
2178                self.prompt_store.clone(),
2179                window,
2180                cx,
2181            )
2182        });
2183
2184        cx.observe(&conversation_view, |this, server_view, cx| {
2185            let is_active = this
2186                .active_conversation_view()
2187                .is_some_and(|active| active.entity_id() == server_view.entity_id());
2188            if is_active {
2189                cx.emit(AgentPanelEvent::ActiveViewChanged);
2190                this.serialize(cx);
2191            } else {
2192                cx.emit(AgentPanelEvent::BackgroundThreadChanged);
2193            }
2194            cx.notify();
2195        })
2196        .detach();
2197
2198        self.set_active_view(
2199            ActiveView::AgentThread { conversation_view },
2200            focus,
2201            window,
2202            cx,
2203        );
2204    }
2205
2206    fn active_thread_has_messages(&self, cx: &App) -> bool {
2207        self.active_agent_thread(cx)
2208            .is_some_and(|thread| !thread.read(cx).entries().is_empty())
2209    }
2210
2211    pub fn active_thread_is_draft(&self, cx: &App) -> bool {
2212        self.active_conversation_view().is_some() && !self.active_thread_has_messages(cx)
2213    }
2214
2215    fn handle_first_send_requested(
2216        &mut self,
2217        thread_view: Entity<ThreadView>,
2218        content: Vec<acp::ContentBlock>,
2219        window: &mut Window,
2220        cx: &mut Context<Self>,
2221    ) {
2222        if self.start_thread_in == StartThreadIn::NewWorktree {
2223            self.handle_worktree_creation_requested(content, window, cx);
2224        } else {
2225            cx.defer_in(window, move |_this, window, cx| {
2226                thread_view.update(cx, |thread_view, cx| {
2227                    let editor = thread_view.message_editor.clone();
2228                    thread_view.send_impl(editor, window, cx);
2229                });
2230            });
2231        }
2232    }
2233
2234    // TODO: The mapping from workspace root paths to git repositories needs a
2235    // unified approach across the codebase: this method, `sidebar::is_root_repo`,
2236    // thread persistence (which PathList is saved to the database), and thread
2237    // querying (which PathList is used to read threads back). All of these need
2238    // to agree on how repos are resolved for a given workspace, especially in
2239    // multi-root and nested-repo configurations.
2240    /// Partitions the project's visible worktrees into git-backed repositories
2241    /// and plain (non-git) paths. Git repos will have worktrees created for
2242    /// them; non-git paths are carried over to the new workspace as-is.
2243    ///
2244    /// When multiple worktrees map to the same repository, the most specific
2245    /// match wins (deepest work directory path), with a deterministic
2246    /// tie-break on entity id. Each repository appears at most once.
2247    fn classify_worktrees(
2248        &self,
2249        cx: &App,
2250    ) -> (Vec<Entity<project::git_store::Repository>>, Vec<PathBuf>) {
2251        let project = &self.project;
2252        let repositories = project.read(cx).repositories(cx).clone();
2253        let mut git_repos: Vec<Entity<project::git_store::Repository>> = Vec::new();
2254        let mut non_git_paths: Vec<PathBuf> = Vec::new();
2255        let mut seen_repo_ids = std::collections::HashSet::new();
2256
2257        for worktree in project.read(cx).visible_worktrees(cx) {
2258            let wt_path = worktree.read(cx).abs_path();
2259
2260            let matching_repo = repositories
2261                .iter()
2262                .filter_map(|(id, repo)| {
2263                    let work_dir = repo.read(cx).work_directory_abs_path.clone();
2264                    if wt_path.starts_with(work_dir.as_ref())
2265                        || work_dir.starts_with(wt_path.as_ref())
2266                    {
2267                        Some((*id, repo.clone(), work_dir.as_ref().components().count()))
2268                    } else {
2269                        None
2270                    }
2271                })
2272                .max_by(
2273                    |(left_id, _left_repo, left_depth), (right_id, _right_repo, right_depth)| {
2274                        left_depth
2275                            .cmp(right_depth)
2276                            .then_with(|| left_id.cmp(right_id))
2277                    },
2278                );
2279
2280            if let Some((id, repo, _)) = matching_repo {
2281                if seen_repo_ids.insert(id) {
2282                    git_repos.push(repo);
2283                }
2284            } else {
2285                non_git_paths.push(wt_path.to_path_buf());
2286            }
2287        }
2288
2289        (git_repos, non_git_paths)
2290    }
2291
2292    /// Kicks off an async git-worktree creation for each repository. Returns:
2293    ///
2294    /// - `creation_infos`: a vec of `(repo, new_path, receiver)` tuples—the
2295    ///   receiver resolves once the git worktree command finishes.
2296    /// - `path_remapping`: `(old_work_dir, new_worktree_path)` pairs used
2297    ///   later to remap open editor tabs into the new workspace.
2298    fn start_worktree_creations(
2299        git_repos: &[Entity<project::git_store::Repository>],
2300        branch_name: &str,
2301        worktree_directory_setting: &str,
2302        cx: &mut Context<Self>,
2303    ) -> Result<(
2304        Vec<(
2305            Entity<project::git_store::Repository>,
2306            PathBuf,
2307            futures::channel::oneshot::Receiver<Result<()>>,
2308        )>,
2309        Vec<(PathBuf, PathBuf)>,
2310    )> {
2311        let mut creation_infos = Vec::new();
2312        let mut path_remapping = Vec::new();
2313
2314        for repo in git_repos {
2315            let (work_dir, new_path, receiver) = repo.update(cx, |repo, _cx| {
2316                let new_path =
2317                    repo.path_for_new_linked_worktree(branch_name, worktree_directory_setting)?;
2318                let receiver =
2319                    repo.create_worktree(branch_name.to_string(), new_path.clone(), None);
2320                let work_dir = repo.work_directory_abs_path.clone();
2321                anyhow::Ok((work_dir, new_path, receiver))
2322            })?;
2323            path_remapping.push((work_dir.to_path_buf(), new_path.clone()));
2324            creation_infos.push((repo.clone(), new_path, receiver));
2325        }
2326
2327        Ok((creation_infos, path_remapping))
2328    }
2329
2330    /// Waits for every in-flight worktree creation to complete. If any
2331    /// creation fails, all successfully-created worktrees are rolled back
2332    /// (removed) so the project isn't left in a half-migrated state.
2333    async fn await_and_rollback_on_failure(
2334        creation_infos: Vec<(
2335            Entity<project::git_store::Repository>,
2336            PathBuf,
2337            futures::channel::oneshot::Receiver<Result<()>>,
2338        )>,
2339        cx: &mut AsyncWindowContext,
2340    ) -> Result<Vec<PathBuf>> {
2341        let mut created_paths: Vec<PathBuf> = Vec::new();
2342        let mut repos_and_paths: Vec<(Entity<project::git_store::Repository>, PathBuf)> =
2343            Vec::new();
2344        let mut first_error: Option<anyhow::Error> = None;
2345
2346        for (repo, new_path, receiver) in creation_infos {
2347            match receiver.await {
2348                Ok(Ok(())) => {
2349                    created_paths.push(new_path.clone());
2350                    repos_and_paths.push((repo, new_path));
2351                }
2352                Ok(Err(err)) => {
2353                    if first_error.is_none() {
2354                        first_error = Some(err);
2355                    }
2356                }
2357                Err(_canceled) => {
2358                    if first_error.is_none() {
2359                        first_error = Some(anyhow!("Worktree creation was canceled"));
2360                    }
2361                }
2362            }
2363        }
2364
2365        let Some(err) = first_error else {
2366            return Ok(created_paths);
2367        };
2368
2369        // Rollback all successfully created worktrees
2370        let mut rollback_receivers = Vec::new();
2371        for (rollback_repo, rollback_path) in &repos_and_paths {
2372            if let Ok(receiver) = cx.update(|_, cx| {
2373                rollback_repo.update(cx, |repo, _cx| {
2374                    repo.remove_worktree(rollback_path.clone(), true)
2375                })
2376            }) {
2377                rollback_receivers.push((rollback_path.clone(), receiver));
2378            }
2379        }
2380        let mut rollback_failures: Vec<String> = Vec::new();
2381        for (path, receiver) in rollback_receivers {
2382            match receiver.await {
2383                Ok(Ok(())) => {}
2384                Ok(Err(rollback_err)) => {
2385                    log::error!(
2386                        "failed to rollback worktree at {}: {rollback_err}",
2387                        path.display()
2388                    );
2389                    rollback_failures.push(format!("{}: {rollback_err}", path.display()));
2390                }
2391                Err(rollback_err) => {
2392                    log::error!(
2393                        "failed to rollback worktree at {}: {rollback_err}",
2394                        path.display()
2395                    );
2396                    rollback_failures.push(format!("{}: {rollback_err}", path.display()));
2397                }
2398            }
2399        }
2400        let mut error_message = format!("Failed to create worktree: {err}");
2401        if !rollback_failures.is_empty() {
2402            error_message.push_str("\n\nFailed to clean up: ");
2403            error_message.push_str(&rollback_failures.join(", "));
2404        }
2405        Err(anyhow!(error_message))
2406    }
2407
2408    fn set_worktree_creation_error(
2409        &mut self,
2410        message: SharedString,
2411        window: &mut Window,
2412        cx: &mut Context<Self>,
2413    ) {
2414        self.worktree_creation_status = Some(WorktreeCreationStatus::Error(message));
2415        if matches!(self.active_view, ActiveView::Uninitialized) {
2416            let selected_agent = self.selected_agent.clone();
2417            self.new_agent_thread(selected_agent, window, cx);
2418        }
2419        cx.notify();
2420    }
2421
2422    fn handle_worktree_creation_requested(
2423        &mut self,
2424        content: Vec<acp::ContentBlock>,
2425        window: &mut Window,
2426        cx: &mut Context<Self>,
2427    ) {
2428        if matches!(
2429            self.worktree_creation_status,
2430            Some(WorktreeCreationStatus::Creating)
2431        ) {
2432            return;
2433        }
2434
2435        self.worktree_creation_status = Some(WorktreeCreationStatus::Creating);
2436        cx.notify();
2437
2438        let (git_repos, non_git_paths) = self.classify_worktrees(cx);
2439
2440        if git_repos.is_empty() {
2441            self.set_worktree_creation_error(
2442                "No git repositories found in the project".into(),
2443                window,
2444                cx,
2445            );
2446            return;
2447        }
2448
2449        // Kick off branch listing as early as possible so it can run
2450        // concurrently with the remaining synchronous setup work.
2451        let branch_receivers: Vec<_> = git_repos
2452            .iter()
2453            .map(|repo| repo.update(cx, |repo, _cx| repo.branches()))
2454            .collect();
2455
2456        let worktree_directory_setting = ProjectSettings::get_global(cx)
2457            .git
2458            .worktree_directory
2459            .clone();
2460
2461        let active_file_path = self.workspace.upgrade().and_then(|workspace| {
2462            let workspace = workspace.read(cx);
2463            let active_item = workspace.active_item(cx)?;
2464            let project_path = active_item.project_path(cx)?;
2465            workspace
2466                .project()
2467                .read(cx)
2468                .absolute_path(&project_path, cx)
2469        });
2470
2471        let workspace = self.workspace.clone();
2472        let window_handle = window
2473            .window_handle()
2474            .downcast::<workspace::MultiWorkspace>();
2475
2476        let selected_agent = self.selected_agent();
2477
2478        let task = cx.spawn_in(window, async move |this, cx| {
2479            // Await the branch listings we kicked off earlier.
2480            let mut existing_branches = Vec::new();
2481            for result in futures::future::join_all(branch_receivers).await {
2482                match result {
2483                    Ok(Ok(branches)) => {
2484                        for branch in branches {
2485                            existing_branches.push(branch.name().to_string());
2486                        }
2487                    }
2488                    Ok(Err(err)) => {
2489                        Err::<(), _>(err).log_err();
2490                    }
2491                    Err(_) => {}
2492                }
2493            }
2494
2495            let existing_branch_refs: Vec<&str> =
2496                existing_branches.iter().map(|s| s.as_str()).collect();
2497            let mut rng = rand::rng();
2498            let branch_name =
2499                match crate::branch_names::generate_branch_name(&existing_branch_refs, &mut rng) {
2500                    Some(name) => name,
2501                    None => {
2502                        this.update_in(cx, |this, window, cx| {
2503                            this.set_worktree_creation_error(
2504                                "Failed to generate a unique branch name".into(),
2505                                window,
2506                                cx,
2507                            );
2508                        })?;
2509                        return anyhow::Ok(());
2510                    }
2511                };
2512
2513            let (creation_infos, path_remapping) = match this.update_in(cx, |_this, _window, cx| {
2514                Self::start_worktree_creations(
2515                    &git_repos,
2516                    &branch_name,
2517                    &worktree_directory_setting,
2518                    cx,
2519                )
2520            }) {
2521                Ok(Ok(result)) => result,
2522                Ok(Err(err)) | Err(err) => {
2523                    this.update_in(cx, |this, window, cx| {
2524                        this.set_worktree_creation_error(
2525                            format!("Failed to validate worktree directory: {err}").into(),
2526                            window,
2527                            cx,
2528                        );
2529                    })
2530                    .log_err();
2531                    return anyhow::Ok(());
2532                }
2533            };
2534
2535            let created_paths = match Self::await_and_rollback_on_failure(creation_infos, cx).await
2536            {
2537                Ok(paths) => paths,
2538                Err(err) => {
2539                    this.update_in(cx, |this, window, cx| {
2540                        this.set_worktree_creation_error(format!("{err}").into(), window, cx);
2541                    })?;
2542                    return anyhow::Ok(());
2543                }
2544            };
2545
2546            let mut all_paths = created_paths;
2547            let has_non_git = !non_git_paths.is_empty();
2548            all_paths.extend(non_git_paths.iter().cloned());
2549
2550            let app_state = match workspace.upgrade() {
2551                Some(workspace) => cx.update(|_, cx| workspace.read(cx).app_state().clone())?,
2552                None => {
2553                    this.update_in(cx, |this, window, cx| {
2554                        this.set_worktree_creation_error(
2555                            "Workspace no longer available".into(),
2556                            window,
2557                            cx,
2558                        );
2559                    })?;
2560                    return anyhow::Ok(());
2561                }
2562            };
2563
2564            let this_for_error = this.clone();
2565            if let Err(err) = Self::setup_new_workspace(
2566                this,
2567                all_paths,
2568                app_state,
2569                window_handle,
2570                active_file_path,
2571                path_remapping,
2572                non_git_paths,
2573                has_non_git,
2574                content,
2575                selected_agent,
2576                cx,
2577            )
2578            .await
2579            {
2580                this_for_error
2581                    .update_in(cx, |this, window, cx| {
2582                        this.set_worktree_creation_error(
2583                            format!("Failed to set up workspace: {err}").into(),
2584                            window,
2585                            cx,
2586                        );
2587                    })
2588                    .log_err();
2589            }
2590            anyhow::Ok(())
2591        });
2592
2593        self._worktree_creation_task = Some(cx.background_spawn(async move {
2594            task.await.log_err();
2595        }));
2596    }
2597
2598    async fn setup_new_workspace(
2599        this: WeakEntity<Self>,
2600        all_paths: Vec<PathBuf>,
2601        app_state: Arc<workspace::AppState>,
2602        window_handle: Option<gpui::WindowHandle<workspace::MultiWorkspace>>,
2603        active_file_path: Option<PathBuf>,
2604        path_remapping: Vec<(PathBuf, PathBuf)>,
2605        non_git_paths: Vec<PathBuf>,
2606        has_non_git: bool,
2607        content: Vec<acp::ContentBlock>,
2608        selected_agent: Option<Agent>,
2609        cx: &mut AsyncWindowContext,
2610    ) -> Result<()> {
2611        let OpenResult {
2612            window: new_window_handle,
2613            workspace: new_workspace,
2614            ..
2615        } = cx
2616            .update(|_window, cx| {
2617                Workspace::new_local(
2618                    all_paths,
2619                    app_state,
2620                    window_handle,
2621                    None,
2622                    None,
2623                    OpenMode::Add,
2624                    cx,
2625                )
2626            })?
2627            .await?;
2628
2629        let panels_task = new_workspace.update(cx, |workspace, _cx| workspace.take_panels_task());
2630
2631        if let Some(task) = panels_task {
2632            task.await.log_err();
2633        }
2634
2635        new_workspace
2636            .update(cx, |workspace, cx| {
2637                workspace.project().read(cx).wait_for_initial_scan(cx)
2638            })
2639            .await;
2640
2641        new_workspace
2642            .update(cx, |workspace, cx| {
2643                let repos = workspace
2644                    .project()
2645                    .read(cx)
2646                    .repositories(cx)
2647                    .values()
2648                    .cloned()
2649                    .collect::<Vec<_>>();
2650
2651                let tasks = repos
2652                    .into_iter()
2653                    .map(|repo| repo.update(cx, |repo, _| repo.barrier()));
2654                futures::future::join_all(tasks)
2655            })
2656            .await;
2657
2658        let initial_content = AgentInitialContent::ContentBlock {
2659            blocks: content,
2660            auto_submit: true,
2661        };
2662
2663        new_window_handle.update(cx, |_multi_workspace, window, cx| {
2664            new_workspace.update(cx, |workspace, cx| {
2665                if has_non_git {
2666                    let toast_id = workspace::notifications::NotificationId::unique::<AgentPanel>();
2667                    workspace.show_toast(
2668                        workspace::Toast::new(
2669                            toast_id,
2670                            "Some project folders are not git repositories. \
2671                             They were included as-is without creating a worktree.",
2672                        ),
2673                        cx,
2674                    );
2675                }
2676
2677                // If we had an active buffer, remap its path and reopen it.
2678                let had_active_file = active_file_path.is_some();
2679                let remapped_active_path = active_file_path.and_then(|original_path| {
2680                    let best_match = path_remapping
2681                        .iter()
2682                        .filter_map(|(old_root, new_root)| {
2683                            original_path.strip_prefix(old_root).ok().map(|relative| {
2684                                (old_root.components().count(), new_root.join(relative))
2685                            })
2686                        })
2687                        .max_by_key(|(depth, _)| *depth);
2688
2689                    if let Some((_, remapped_path)) = best_match {
2690                        return Some(remapped_path);
2691                    }
2692
2693                    for non_git in &non_git_paths {
2694                        if original_path.starts_with(non_git) {
2695                            return Some(original_path);
2696                        }
2697                    }
2698                    None
2699                });
2700
2701                if had_active_file && remapped_active_path.is_none() {
2702                    log::warn!(
2703                        "Active file could not be remapped to the new worktree; it will not be reopened"
2704                    );
2705                }
2706
2707                if let Some(path) = remapped_active_path {
2708                    let open_task = workspace.open_paths(
2709                        vec![path],
2710                        workspace::OpenOptions::default(),
2711                        None,
2712                        window,
2713                        cx,
2714                    );
2715                    cx.spawn(async move |_, _| -> anyhow::Result<()> {
2716                        for item in open_task.await.into_iter().flatten() {
2717                            item?;
2718                        }
2719                        Ok(())
2720                    })
2721                    .detach_and_log_err(cx);
2722                }
2723
2724                workspace.focus_panel::<AgentPanel>(window, cx);
2725
2726                // If no active buffer was open, zoom the agent panel
2727                // (equivalent to cmd-esc fullscreen behavior).
2728                // This must happen after focus_panel, which activates
2729                // and opens the panel in the dock.
2730
2731                if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
2732                    panel.update(cx, |panel, cx| {
2733                        panel.external_thread(
2734                            selected_agent,
2735                            None,
2736                            None,
2737                            None,
2738                            Some(initial_content),
2739                            true,
2740                            window,
2741                            cx,
2742                        );
2743                    });
2744                }
2745            });
2746        })?;
2747
2748        new_window_handle.update(cx, |multi_workspace, window, cx| {
2749            multi_workspace.activate(new_workspace.clone(), window, cx);
2750
2751            new_workspace.update(cx, |workspace, cx| {
2752                workspace.run_create_worktree_tasks(window, cx);
2753            })
2754        })?;
2755
2756        this.update_in(cx, |this, window, cx| {
2757            this.worktree_creation_status = None;
2758
2759            if let Some(thread_view) = this.active_thread_view(cx) {
2760                thread_view.update(cx, |thread_view, cx| {
2761                    thread_view
2762                        .message_editor
2763                        .update(cx, |editor, cx| editor.clear(window, cx));
2764                });
2765            }
2766
2767            cx.notify();
2768        })?;
2769
2770        anyhow::Ok(())
2771    }
2772}
2773
2774impl Focusable for AgentPanel {
2775    fn focus_handle(&self, cx: &App) -> FocusHandle {
2776        match &self.active_view {
2777            ActiveView::Uninitialized => self.focus_handle.clone(),
2778            ActiveView::AgentThread {
2779                conversation_view, ..
2780            } => conversation_view.focus_handle(cx),
2781            ActiveView::History { view } => view.read(cx).focus_handle(cx),
2782            ActiveView::Configuration => {
2783                if let Some(configuration) = self.configuration.as_ref() {
2784                    configuration.focus_handle(cx)
2785                } else {
2786                    self.focus_handle.clone()
2787                }
2788            }
2789        }
2790    }
2791}
2792
2793fn agent_panel_dock_position(cx: &App) -> DockPosition {
2794    AgentSettings::get_global(cx).dock.into()
2795}
2796
2797pub enum AgentPanelEvent {
2798    ActiveViewChanged,
2799    ThreadFocused,
2800    BackgroundThreadChanged,
2801    MessageSentOrQueued { session_id: acp::SessionId },
2802}
2803
2804impl EventEmitter<PanelEvent> for AgentPanel {}
2805impl EventEmitter<AgentPanelEvent> for AgentPanel {}
2806
2807impl Panel for AgentPanel {
2808    fn persistent_name() -> &'static str {
2809        "AgentPanel"
2810    }
2811
2812    fn panel_key() -> &'static str {
2813        AGENT_PANEL_KEY
2814    }
2815
2816    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
2817        agent_panel_dock_position(cx)
2818    }
2819
2820    fn position_is_valid(&self, position: DockPosition) -> bool {
2821        position != DockPosition::Bottom
2822    }
2823
2824    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
2825        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
2826            settings
2827                .agent
2828                .get_or_insert_default()
2829                .set_dock(position.into());
2830        });
2831    }
2832
2833    fn default_size(&self, window: &Window, cx: &App) -> Pixels {
2834        let settings = AgentSettings::get_global(cx);
2835        match self.position(window, cx) {
2836            DockPosition::Left | DockPosition::Right => settings.default_width,
2837            DockPosition::Bottom => settings.default_height,
2838        }
2839    }
2840
2841    fn supports_flexible_size(&self) -> bool {
2842        true
2843    }
2844
2845    fn has_flexible_size(&self, _window: &Window, cx: &App) -> bool {
2846        AgentSettings::get_global(cx).flexible
2847    }
2848
2849    fn set_flexible_size(&mut self, flexible: bool, _window: &mut Window, cx: &mut Context<Self>) {
2850        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
2851            settings
2852                .agent
2853                .get_or_insert_default()
2854                .set_flexible_size(flexible);
2855        });
2856    }
2857
2858    fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
2859        if active
2860            && matches!(self.active_view, ActiveView::Uninitialized)
2861            && !matches!(
2862                self.worktree_creation_status,
2863                Some(WorktreeCreationStatus::Creating)
2864            )
2865        {
2866            let selected_agent = self.selected_agent.clone();
2867            self.new_agent_thread_inner(selected_agent, false, window, cx);
2868        }
2869    }
2870
2871    fn remote_id() -> Option<proto::PanelId> {
2872        Some(proto::PanelId::AssistantPanel)
2873    }
2874
2875    fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
2876        (self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAssistant)
2877    }
2878
2879    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
2880        Some("Agent Panel")
2881    }
2882
2883    fn toggle_action(&self) -> Box<dyn Action> {
2884        Box::new(ToggleFocus)
2885    }
2886
2887    fn activation_priority(&self) -> u32 {
2888        0
2889    }
2890
2891    fn enabled(&self, cx: &App) -> bool {
2892        AgentSettings::get_global(cx).enabled(cx)
2893    }
2894
2895    fn is_agent_panel(&self) -> bool {
2896        true
2897    }
2898
2899    fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
2900        self.zoomed
2901    }
2902
2903    fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
2904        self.zoomed = zoomed;
2905        cx.notify();
2906    }
2907}
2908
2909impl AgentPanel {
2910    fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
2911        let content = match &self.active_view {
2912            ActiveView::AgentThread { conversation_view } => {
2913                let server_view_ref = conversation_view.read(cx);
2914                let is_generating_title = server_view_ref.as_native_thread(cx).is_some()
2915                    && server_view_ref.root_thread(cx).map_or(false, |tv| {
2916                        tv.read(cx).thread.read(cx).has_provisional_title()
2917                    });
2918
2919                if let Some(title_editor) = server_view_ref
2920                    .root_thread(cx)
2921                    .map(|r| r.read(cx).title_editor.clone())
2922                {
2923                    if is_generating_title {
2924                        Label::new(DEFAULT_THREAD_TITLE)
2925                            .color(Color::Muted)
2926                            .truncate()
2927                            .with_animation(
2928                                "generating_title",
2929                                Animation::new(Duration::from_secs(2))
2930                                    .repeat()
2931                                    .with_easing(pulsating_between(0.4, 0.8)),
2932                                |label, delta| label.alpha(delta),
2933                            )
2934                            .into_any_element()
2935                    } else {
2936                        div()
2937                            .w_full()
2938                            .on_action({
2939                                let conversation_view = conversation_view.downgrade();
2940                                move |_: &menu::Confirm, window, cx| {
2941                                    if let Some(conversation_view) = conversation_view.upgrade() {
2942                                        conversation_view.focus_handle(cx).focus(window, cx);
2943                                    }
2944                                }
2945                            })
2946                            .on_action({
2947                                let conversation_view = conversation_view.downgrade();
2948                                move |_: &editor::actions::Cancel, window, cx| {
2949                                    if let Some(conversation_view) = conversation_view.upgrade() {
2950                                        conversation_view.focus_handle(cx).focus(window, cx);
2951                                    }
2952                                }
2953                            })
2954                            .child(title_editor)
2955                            .into_any_element()
2956                    }
2957                } else {
2958                    Label::new(conversation_view.read(cx).title(cx))
2959                        .color(Color::Muted)
2960                        .truncate()
2961                        .into_any_element()
2962                }
2963            }
2964            ActiveView::History { .. } => Label::new("History").truncate().into_any_element(),
2965            ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
2966            ActiveView::Uninitialized => Label::new("Agent").truncate().into_any_element(),
2967        };
2968
2969        h_flex()
2970            .key_context("TitleEditor")
2971            .id("TitleEditor")
2972            .flex_grow()
2973            .w_full()
2974            .max_w_full()
2975            .overflow_x_scroll()
2976            .child(content)
2977            .into_any()
2978    }
2979
2980    fn handle_regenerate_thread_title(conversation_view: Entity<ConversationView>, cx: &mut App) {
2981        conversation_view.update(cx, |conversation_view, cx| {
2982            if let Some(thread) = conversation_view.as_native_thread(cx) {
2983                thread.update(cx, |thread, cx| {
2984                    thread.generate_title(cx);
2985                });
2986            }
2987        });
2988    }
2989
2990    fn render_panel_options_menu(
2991        &self,
2992        window: &mut Window,
2993        cx: &mut Context<Self>,
2994    ) -> impl IntoElement {
2995        let focus_handle = self.focus_handle(cx);
2996
2997        let full_screen_label = if self.is_zoomed(window, cx) {
2998            "Disable Full Screen"
2999        } else {
3000            "Enable Full Screen"
3001        };
3002
3003        let conversation_view = match &self.active_view {
3004            ActiveView::AgentThread { conversation_view } => Some(conversation_view.clone()),
3005            _ => None,
3006        };
3007        let thread_with_messages = match &self.active_view {
3008            ActiveView::AgentThread { conversation_view } => {
3009                conversation_view.read(cx).has_user_submitted_prompt(cx)
3010            }
3011            _ => false,
3012        };
3013        let has_auth_methods = match &self.active_view {
3014            ActiveView::AgentThread { conversation_view } => {
3015                conversation_view.read(cx).has_auth_methods()
3016            }
3017            _ => false,
3018        };
3019
3020        PopoverMenu::new("agent-options-menu")
3021            .trigger_with_tooltip(
3022                IconButton::new("agent-options-menu", IconName::Ellipsis)
3023                    .icon_size(IconSize::Small),
3024                {
3025                    let focus_handle = focus_handle.clone();
3026                    move |_window, cx| {
3027                        Tooltip::for_action_in(
3028                            "Toggle Agent Menu",
3029                            &ToggleOptionsMenu,
3030                            &focus_handle,
3031                            cx,
3032                        )
3033                    }
3034                },
3035            )
3036            .anchor(Corner::TopRight)
3037            .with_handle(self.agent_panel_menu_handle.clone())
3038            .menu({
3039                move |window, cx| {
3040                    Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
3041                        menu = menu.context(focus_handle.clone());
3042
3043                        if thread_with_messages {
3044                            menu = menu.header("Current Thread");
3045
3046                            if let Some(conversation_view) = conversation_view.as_ref() {
3047                                menu = menu
3048                                    .entry("Regenerate Thread Title", None, {
3049                                        let conversation_view = conversation_view.clone();
3050                                        move |_, cx| {
3051                                            Self::handle_regenerate_thread_title(
3052                                                conversation_view.clone(),
3053                                                cx,
3054                                            );
3055                                        }
3056                                    })
3057                                    .separator();
3058                            }
3059                        }
3060
3061                        menu = menu
3062                            .header("MCP Servers")
3063                            .action(
3064                                "View Server Extensions",
3065                                Box::new(zed_actions::Extensions {
3066                                    category_filter: Some(
3067                                        zed_actions::ExtensionCategoryFilter::ContextServers,
3068                                    ),
3069                                    id: None,
3070                                }),
3071                            )
3072                            .action("Add Custom Server…", Box::new(AddContextServer))
3073                            .separator()
3074                            .action("Rules", Box::new(OpenRulesLibrary::default()))
3075                            .action("Profiles", Box::new(ManageProfiles::default()))
3076                            .action("Settings", Box::new(OpenSettings))
3077                            .separator()
3078                            .action("Toggle Threads Sidebar", Box::new(ToggleWorkspaceSidebar))
3079                            .action(full_screen_label, Box::new(ToggleZoom));
3080
3081                        if has_auth_methods {
3082                            menu = menu.action("Reauthenticate", Box::new(ReauthenticateAgent))
3083                        }
3084
3085                        menu
3086                    }))
3087                }
3088            })
3089    }
3090
3091    fn render_recent_entries_menu(
3092        &self,
3093        icon: IconName,
3094        corner: Corner,
3095        cx: &mut Context<Self>,
3096    ) -> impl IntoElement {
3097        let focus_handle = self.focus_handle(cx);
3098
3099        PopoverMenu::new("agent-nav-menu")
3100            .trigger_with_tooltip(
3101                IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small),
3102                {
3103                    move |_window, cx| {
3104                        Tooltip::for_action_in(
3105                            "Toggle Recently Updated Threads",
3106                            &ToggleNavigationMenu,
3107                            &focus_handle,
3108                            cx,
3109                        )
3110                    }
3111                },
3112            )
3113            .anchor(corner)
3114            .with_handle(self.agent_navigation_menu_handle.clone())
3115            .menu({
3116                let menu = self.agent_navigation_menu.clone();
3117                move |window, cx| {
3118                    telemetry::event!("View Thread History Clicked");
3119
3120                    if let Some(menu) = menu.as_ref() {
3121                        menu.update(cx, |_, cx| {
3122                            cx.defer_in(window, |menu, window, cx| {
3123                                menu.rebuild(window, cx);
3124                            });
3125                        })
3126                    }
3127                    menu.clone()
3128                }
3129            })
3130    }
3131
3132    fn render_toolbar_back_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
3133        let focus_handle = self.focus_handle(cx);
3134
3135        IconButton::new("go-back", IconName::ArrowLeft)
3136            .icon_size(IconSize::Small)
3137            .on_click(cx.listener(|this, _, window, cx| {
3138                this.go_back(&workspace::GoBack, window, cx);
3139            }))
3140            .tooltip({
3141                move |_window, cx| {
3142                    Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, cx)
3143                }
3144            })
3145    }
3146
3147    fn project_has_git_repository(&self, cx: &App) -> bool {
3148        !self.project.read(cx).repositories(cx).is_empty()
3149    }
3150
3151    fn render_start_thread_in_selector(&self, cx: &mut Context<Self>) -> impl IntoElement {
3152        use settings::{NewThreadLocation, Settings};
3153
3154        let focus_handle = self.focus_handle(cx);
3155        let has_git_repo = self.project_has_git_repository(cx);
3156        let is_via_collab = self.project.read(cx).is_via_collab();
3157        let fs = self.fs.clone();
3158
3159        let is_creating = matches!(
3160            self.worktree_creation_status,
3161            Some(WorktreeCreationStatus::Creating)
3162        );
3163
3164        let current_target = self.start_thread_in;
3165        let trigger_label = self.start_thread_in.label();
3166
3167        let new_thread_location = AgentSettings::get_global(cx).new_thread_location;
3168        let is_local_default = new_thread_location == NewThreadLocation::LocalProject;
3169        let is_new_worktree_default = new_thread_location == NewThreadLocation::NewWorktree;
3170
3171        let icon = if self.start_thread_in_menu_handle.is_deployed() {
3172            IconName::ChevronUp
3173        } else {
3174            IconName::ChevronDown
3175        };
3176
3177        let trigger_button = Button::new("thread-target-trigger", trigger_label)
3178            .end_icon(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
3179            .disabled(is_creating);
3180
3181        let dock_position = AgentSettings::get_global(cx).dock;
3182        let documentation_side = match dock_position {
3183            settings::DockPosition::Left => DocumentationSide::Right,
3184            settings::DockPosition::Bottom | settings::DockPosition::Right => {
3185                DocumentationSide::Left
3186            }
3187        };
3188
3189        PopoverMenu::new("thread-target-selector")
3190            .trigger_with_tooltip(trigger_button, {
3191                move |_window, cx| {
3192                    Tooltip::for_action_in(
3193                        "Start Thread In…",
3194                        &CycleStartThreadIn,
3195                        &focus_handle,
3196                        cx,
3197                    )
3198                }
3199            })
3200            .menu(move |window, cx| {
3201                let is_local_selected = current_target == StartThreadIn::LocalProject;
3202                let is_new_worktree_selected = current_target == StartThreadIn::NewWorktree;
3203                let fs = fs.clone();
3204
3205                Some(ContextMenu::build(window, cx, move |menu, _window, _cx| {
3206                    let new_worktree_disabled = !has_git_repo || is_via_collab;
3207
3208                    menu.header("Start Thread In…")
3209                        .item(
3210                            ContextMenuEntry::new("Current Worktree")
3211                                .toggleable(IconPosition::End, is_local_selected)
3212                                .documentation_aside(documentation_side, move |_| {
3213                                    HoldForDefault::new(is_local_default)
3214                                        .more_content(false)
3215                                        .into_any_element()
3216                                })
3217                                .handler({
3218                                    let fs = fs.clone();
3219                                    move |window, cx| {
3220                                        if window.modifiers().secondary() {
3221                                            update_settings_file(fs.clone(), cx, |settings, _| {
3222                                                settings
3223                                                    .agent
3224                                                    .get_or_insert_default()
3225                                                    .set_new_thread_location(
3226                                                        NewThreadLocation::LocalProject,
3227                                                    );
3228                                            });
3229                                        }
3230                                        window.dispatch_action(
3231                                            Box::new(StartThreadIn::LocalProject),
3232                                            cx,
3233                                        );
3234                                    }
3235                                }),
3236                        )
3237                        .item({
3238                            let entry = ContextMenuEntry::new("New Git Worktree")
3239                                .toggleable(IconPosition::End, is_new_worktree_selected)
3240                                .disabled(new_worktree_disabled)
3241                                .handler({
3242                                    let fs = fs.clone();
3243                                    move |window, cx| {
3244                                        if window.modifiers().secondary() {
3245                                            update_settings_file(fs.clone(), cx, |settings, _| {
3246                                                settings
3247                                                    .agent
3248                                                    .get_or_insert_default()
3249                                                    .set_new_thread_location(
3250                                                        NewThreadLocation::NewWorktree,
3251                                                    );
3252                                            });
3253                                        }
3254                                        window.dispatch_action(
3255                                            Box::new(StartThreadIn::NewWorktree),
3256                                            cx,
3257                                        );
3258                                    }
3259                                });
3260
3261                            if new_worktree_disabled {
3262                                entry.documentation_aside(documentation_side, move |_| {
3263                                    let reason = if !has_git_repo {
3264                                        "No git repository found in this project."
3265                                    } else {
3266                                        "Not available for remote/collab projects yet."
3267                                    };
3268                                    Label::new(reason)
3269                                        .color(Color::Muted)
3270                                        .size(LabelSize::Small)
3271                                        .into_any_element()
3272                                })
3273                            } else {
3274                                entry.documentation_aside(documentation_side, move |_| {
3275                                    HoldForDefault::new(is_new_worktree_default)
3276                                        .more_content(false)
3277                                        .into_any_element()
3278                                })
3279                            }
3280                        })
3281                }))
3282            })
3283            .with_handle(self.start_thread_in_menu_handle.clone())
3284            .anchor(Corner::TopLeft)
3285            .offset(gpui::Point {
3286                x: px(1.0),
3287                y: px(1.0),
3288            })
3289    }
3290
3291    fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3292        let agent_server_store = self.project.read(cx).agent_server_store().clone();
3293        let has_visible_worktrees = self.project.read(cx).visible_worktrees(cx).next().is_some();
3294        let focus_handle = self.focus_handle(cx);
3295
3296        let (selected_agent_custom_icon, selected_agent_label) =
3297            if let Agent::Custom { id, .. } = &self.selected_agent {
3298                let store = agent_server_store.read(cx);
3299                let icon = store.agent_icon(&id);
3300
3301                let label = store
3302                    .agent_display_name(&id)
3303                    .unwrap_or_else(|| self.selected_agent.label());
3304                (icon, label)
3305            } else {
3306                (None, self.selected_agent.label())
3307            };
3308
3309        let active_thread = match &self.active_view {
3310            ActiveView::AgentThread { conversation_view } => {
3311                conversation_view.read(cx).as_native_thread(cx)
3312            }
3313            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {
3314                None
3315            }
3316        };
3317
3318        let new_thread_menu_builder: Rc<
3319            dyn Fn(&mut Window, &mut App) -> Option<Entity<ContextMenu>>,
3320        > = {
3321            let selected_agent = self.selected_agent.clone();
3322            let is_agent_selected = move |agent: Agent| selected_agent == agent;
3323
3324            let workspace = self.workspace.clone();
3325            let is_via_collab = workspace
3326                .update(cx, |workspace, cx| {
3327                    workspace.project().read(cx).is_via_collab()
3328                })
3329                .unwrap_or_default();
3330
3331            let focus_handle = focus_handle.clone();
3332            let agent_server_store = agent_server_store;
3333
3334            Rc::new(move |window, cx| {
3335                telemetry::event!("New Thread Clicked");
3336
3337                let active_thread = active_thread.clone();
3338                Some(ContextMenu::build(window, cx, |menu, _window, cx| {
3339                    menu.context(focus_handle.clone())
3340                        .when_some(active_thread, |this, active_thread| {
3341                            let thread = active_thread.read(cx);
3342
3343                            if !thread.is_empty() {
3344                                let session_id = thread.id().clone();
3345                                this.item(
3346                                    ContextMenuEntry::new("New From Summary")
3347                                        .icon(IconName::ThreadFromSummary)
3348                                        .icon_color(Color::Muted)
3349                                        .handler(move |window, cx| {
3350                                            window.dispatch_action(
3351                                                Box::new(NewNativeAgentThreadFromSummary {
3352                                                    from_session_id: session_id.clone(),
3353                                                }),
3354                                                cx,
3355                                            );
3356                                        }),
3357                                )
3358                            } else {
3359                                this
3360                            }
3361                        })
3362                        .item(
3363                            ContextMenuEntry::new("Zed Agent")
3364                                .when(is_agent_selected(Agent::NativeAgent), |this| {
3365                                    this.action(Box::new(NewExternalAgentThread { agent: None }))
3366                                })
3367                                .icon(IconName::ZedAgent)
3368                                .icon_color(Color::Muted)
3369                                .handler({
3370                                    let workspace = workspace.clone();
3371                                    move |window, cx| {
3372                                        if let Some(workspace) = workspace.upgrade() {
3373                                            workspace.update(cx, |workspace, cx| {
3374                                                if let Some(panel) =
3375                                                    workspace.panel::<AgentPanel>(cx)
3376                                                {
3377                                                    panel.update(cx, |panel, cx| {
3378                                                        panel.new_agent_thread(
3379                                                            Agent::NativeAgent,
3380                                                            window,
3381                                                            cx,
3382                                                        );
3383                                                    });
3384                                                }
3385                                            });
3386                                        }
3387                                    }
3388                                }),
3389                        )
3390                        .map(|mut menu| {
3391                            let agent_server_store = agent_server_store.read(cx);
3392                            let registry_store = project::AgentRegistryStore::try_global(cx);
3393                            let registry_store_ref = registry_store.as_ref().map(|s| s.read(cx));
3394
3395                            struct AgentMenuItem {
3396                                id: AgentId,
3397                                display_name: SharedString,
3398                            }
3399
3400                            let agent_items = agent_server_store
3401                                .external_agents()
3402                                .map(|agent_id| {
3403                                    let display_name = agent_server_store
3404                                        .agent_display_name(agent_id)
3405                                        .or_else(|| {
3406                                            registry_store_ref
3407                                                .as_ref()
3408                                                .and_then(|store| store.agent(agent_id))
3409                                                .map(|a| a.name().clone())
3410                                        })
3411                                        .unwrap_or_else(|| agent_id.0.clone());
3412                                    AgentMenuItem {
3413                                        id: agent_id.clone(),
3414                                        display_name,
3415                                    }
3416                                })
3417                                .sorted_unstable_by_key(|e| e.display_name.to_lowercase())
3418                                .collect::<Vec<_>>();
3419
3420                            if !agent_items.is_empty() {
3421                                menu = menu.separator().header("External Agents");
3422                            }
3423                            for item in &agent_items {
3424                                let mut entry = ContextMenuEntry::new(item.display_name.clone());
3425
3426                                let icon_path =
3427                                    agent_server_store.agent_icon(&item.id).or_else(|| {
3428                                        registry_store_ref
3429                                            .as_ref()
3430                                            .and_then(|store| store.agent(&item.id))
3431                                            .and_then(|a| a.icon_path().cloned())
3432                                    });
3433
3434                                if let Some(icon_path) = icon_path {
3435                                    entry = entry.custom_icon_svg(icon_path);
3436                                } else {
3437                                    entry = entry.icon(IconName::Sparkle);
3438                                }
3439
3440                                entry = entry
3441                                    .when(
3442                                        is_agent_selected(Agent::Custom {
3443                                            id: item.id.clone(),
3444                                        }),
3445                                        |this| {
3446                                            this.action(Box::new(NewExternalAgentThread {
3447                                                agent: None,
3448                                            }))
3449                                        },
3450                                    )
3451                                    .icon_color(Color::Muted)
3452                                    .disabled(is_via_collab)
3453                                    .handler({
3454                                        let workspace = workspace.clone();
3455                                        let agent_id = item.id.clone();
3456                                        move |window, cx| {
3457                                            if let Some(workspace) = workspace.upgrade() {
3458                                                workspace.update(cx, |workspace, cx| {
3459                                                    if let Some(panel) =
3460                                                        workspace.panel::<AgentPanel>(cx)
3461                                                    {
3462                                                        panel.update(cx, |panel, cx| {
3463                                                            panel.new_agent_thread(
3464                                                                Agent::Custom {
3465                                                                    id: agent_id.clone(),
3466                                                                },
3467                                                                window,
3468                                                                cx,
3469                                                            );
3470                                                        });
3471                                                    }
3472                                                });
3473                                            }
3474                                        }
3475                                    });
3476
3477                                menu = menu.item(entry);
3478                            }
3479
3480                            menu
3481                        })
3482                        .separator()
3483                        .item(
3484                            ContextMenuEntry::new("Add More Agents")
3485                                .icon(IconName::Plus)
3486                                .icon_color(Color::Muted)
3487                                .handler({
3488                                    move |window, cx| {
3489                                        window
3490                                            .dispatch_action(Box::new(zed_actions::AcpRegistry), cx)
3491                                    }
3492                                }),
3493                        )
3494                }))
3495            })
3496        };
3497
3498        let is_thread_loading = self
3499            .active_conversation_view()
3500            .map(|thread| thread.read(cx).is_loading())
3501            .unwrap_or(false);
3502
3503        let has_custom_icon = selected_agent_custom_icon.is_some();
3504        let selected_agent_custom_icon_for_button = selected_agent_custom_icon.clone();
3505        let selected_agent_builtin_icon = self.selected_agent.icon();
3506        let selected_agent_label_for_tooltip = selected_agent_label.clone();
3507
3508        let selected_agent = div()
3509            .id("selected_agent_icon")
3510            .when_some(selected_agent_custom_icon, |this, icon_path| {
3511                this.px_1()
3512                    .child(Icon::from_external_svg(icon_path).color(Color::Muted))
3513            })
3514            .when(!has_custom_icon, |this| {
3515                this.when_some(selected_agent_builtin_icon, |this, icon| {
3516                    this.px_1().child(Icon::new(icon).color(Color::Muted))
3517                })
3518            })
3519            .tooltip(move |_, cx| {
3520                Tooltip::with_meta(
3521                    selected_agent_label_for_tooltip.clone(),
3522                    None,
3523                    "Selected Agent",
3524                    cx,
3525                )
3526            });
3527
3528        let selected_agent = if is_thread_loading {
3529            selected_agent
3530                .with_animation(
3531                    "pulsating-icon",
3532                    Animation::new(Duration::from_secs(1))
3533                        .repeat()
3534                        .with_easing(pulsating_between(0.2, 0.6)),
3535                    |icon, delta| icon.opacity(delta),
3536                )
3537                .into_any_element()
3538        } else {
3539            selected_agent.into_any_element()
3540        };
3541
3542        let show_history_menu = self.has_history_for_selected_agent(cx);
3543        let has_v2_flag = cx.has_flag::<AgentV2FeatureFlag>();
3544        let is_empty_state = !self.active_thread_has_messages(cx);
3545
3546        let is_in_history_or_config = matches!(
3547            &self.active_view,
3548            ActiveView::History { .. } | ActiveView::Configuration
3549        );
3550
3551        let is_full_screen = self.is_zoomed(window, cx);
3552
3553        let use_v2_empty_toolbar = has_v2_flag && is_empty_state && !is_in_history_or_config;
3554
3555        let base_container = h_flex()
3556            .id("agent-panel-toolbar")
3557            .h(Tab::container_height(cx))
3558            .max_w_full()
3559            .flex_none()
3560            .justify_between()
3561            .gap_2()
3562            .bg(cx.theme().colors().tab_bar_background)
3563            .border_b_1()
3564            .border_color(cx.theme().colors().border);
3565
3566        if use_v2_empty_toolbar {
3567            let (chevron_icon, icon_color, label_color) =
3568                if self.new_thread_menu_handle.is_deployed() {
3569                    (IconName::ChevronUp, Color::Accent, Color::Accent)
3570                } else {
3571                    (IconName::ChevronDown, Color::Muted, Color::Default)
3572                };
3573
3574            let agent_icon = if let Some(icon_path) = selected_agent_custom_icon_for_button {
3575                Icon::from_external_svg(icon_path)
3576                    .size(IconSize::Small)
3577                    .color(icon_color)
3578            } else {
3579                let icon_name = selected_agent_builtin_icon.unwrap_or(IconName::ZedAgent);
3580                Icon::new(icon_name).size(IconSize::Small).color(icon_color)
3581            };
3582
3583            let agent_selector_button = Button::new("agent-selector-trigger", selected_agent_label)
3584                .start_icon(agent_icon)
3585                .color(label_color)
3586                .end_icon(
3587                    Icon::new(chevron_icon)
3588                        .color(icon_color)
3589                        .size(IconSize::XSmall),
3590                );
3591
3592            let agent_selector_menu = PopoverMenu::new("new_thread_menu")
3593                .trigger_with_tooltip(agent_selector_button, {
3594                    move |_window, cx| {
3595                        Tooltip::for_action_in(
3596                            "New Thread…",
3597                            &ToggleNewThreadMenu,
3598                            &focus_handle,
3599                            cx,
3600                        )
3601                    }
3602                })
3603                .menu({
3604                    let builder = new_thread_menu_builder.clone();
3605                    move |window, cx| builder(window, cx)
3606                })
3607                .with_handle(self.new_thread_menu_handle.clone())
3608                .anchor(Corner::TopLeft)
3609                .offset(gpui::Point {
3610                    x: px(1.0),
3611                    y: px(1.0),
3612                });
3613
3614            base_container
3615                .child(
3616                    h_flex()
3617                        .size_full()
3618                        .gap(DynamicSpacing::Base04.rems(cx))
3619                        .pl(DynamicSpacing::Base04.rems(cx))
3620                        .child(agent_selector_menu)
3621                        .when(
3622                            has_visible_worktrees && self.project_has_git_repository(cx),
3623                            |this| this.child(self.render_start_thread_in_selector(cx)),
3624                        ),
3625                )
3626                .child(
3627                    h_flex()
3628                        .h_full()
3629                        .flex_none()
3630                        .gap_1()
3631                        .pl_1()
3632                        .pr_1()
3633                        .when(show_history_menu && !has_v2_flag, |this| {
3634                            this.child(self.render_recent_entries_menu(
3635                                IconName::MenuAltTemp,
3636                                Corner::TopRight,
3637                                cx,
3638                            ))
3639                        })
3640                        .when(is_full_screen, |this| {
3641                            this.child(
3642                                IconButton::new("disable-full-screen", IconName::Minimize)
3643                                    .icon_size(IconSize::Small)
3644                                    .tooltip(move |_, cx| {
3645                                        Tooltip::for_action("Disable Full Screen", &ToggleZoom, cx)
3646                                    })
3647                                    .on_click({
3648                                        cx.listener(move |_, _, window, cx| {
3649                                            window.dispatch_action(ToggleZoom.boxed_clone(), cx);
3650                                        })
3651                                    }),
3652                            )
3653                        })
3654                        .child(self.render_panel_options_menu(window, cx)),
3655                )
3656                .into_any_element()
3657        } else {
3658            let new_thread_menu = PopoverMenu::new("new_thread_menu")
3659                .trigger_with_tooltip(
3660                    IconButton::new("new_thread_menu_btn", IconName::Plus)
3661                        .icon_size(IconSize::Small),
3662                    {
3663                        move |_window, cx| {
3664                            Tooltip::for_action_in(
3665                                "New Thread\u{2026}",
3666                                &ToggleNewThreadMenu,
3667                                &focus_handle,
3668                                cx,
3669                            )
3670                        }
3671                    },
3672                )
3673                .anchor(Corner::TopRight)
3674                .with_handle(self.new_thread_menu_handle.clone())
3675                .menu(move |window, cx| new_thread_menu_builder(window, cx));
3676
3677            base_container
3678                .child(
3679                    h_flex()
3680                        .size_full()
3681                        .gap(DynamicSpacing::Base04.rems(cx))
3682                        .pl(DynamicSpacing::Base04.rems(cx))
3683                        .child(match &self.active_view {
3684                            ActiveView::History { .. } | ActiveView::Configuration => {
3685                                self.render_toolbar_back_button(cx).into_any_element()
3686                            }
3687                            _ => selected_agent.into_any_element(),
3688                        })
3689                        .child(self.render_title_view(window, cx)),
3690                )
3691                .child(
3692                    h_flex()
3693                        .h_full()
3694                        .flex_none()
3695                        .gap_1()
3696                        .pl_1()
3697                        .pr_1()
3698                        .child(new_thread_menu)
3699                        .when(show_history_menu && !has_v2_flag, |this| {
3700                            this.child(self.render_recent_entries_menu(
3701                                IconName::MenuAltTemp,
3702                                Corner::TopRight,
3703                                cx,
3704                            ))
3705                        })
3706                        .when(is_full_screen, |this| {
3707                            this.child(
3708                                IconButton::new("disable-full-screen", IconName::Minimize)
3709                                    .icon_size(IconSize::Small)
3710                                    .tooltip(move |_, cx| {
3711                                        Tooltip::for_action("Disable Full Screen", &ToggleZoom, cx)
3712                                    })
3713                                    .on_click({
3714                                        cx.listener(move |_, _, window, cx| {
3715                                            window.dispatch_action(ToggleZoom.boxed_clone(), cx);
3716                                        })
3717                                    }),
3718                            )
3719                        })
3720                        .child(self.render_panel_options_menu(window, cx)),
3721                )
3722                .into_any_element()
3723        }
3724    }
3725
3726    fn render_worktree_creation_status(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
3727        let status = self.worktree_creation_status.as_ref()?;
3728        match status {
3729            WorktreeCreationStatus::Creating => Some(
3730                h_flex()
3731                    .absolute()
3732                    .bottom_12()
3733                    .w_full()
3734                    .p_2()
3735                    .gap_1()
3736                    .justify_center()
3737                    .bg(cx.theme().colors().editor_background)
3738                    .child(
3739                        Icon::new(IconName::LoadCircle)
3740                            .size(IconSize::Small)
3741                            .color(Color::Muted)
3742                            .with_rotate_animation(3),
3743                    )
3744                    .child(
3745                        Label::new("Creating Worktree…")
3746                            .color(Color::Muted)
3747                            .size(LabelSize::Small),
3748                    )
3749                    .into_any_element(),
3750            ),
3751            WorktreeCreationStatus::Error(message) => Some(
3752                Callout::new()
3753                    .icon(IconName::Warning)
3754                    .severity(Severity::Warning)
3755                    .title(message.clone())
3756                    .into_any_element(),
3757            ),
3758        }
3759    }
3760
3761    fn should_render_trial_end_upsell(&self, cx: &mut Context<Self>) -> bool {
3762        if TrialEndUpsell::dismissed(cx) {
3763            return false;
3764        }
3765
3766        match &self.active_view {
3767            ActiveView::AgentThread { .. } => {
3768                if LanguageModelRegistry::global(cx)
3769                    .read(cx)
3770                    .default_model()
3771                    .is_some_and(|model| {
3772                        model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
3773                    })
3774                {
3775                    return false;
3776                }
3777            }
3778            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {
3779                return false;
3780            }
3781        }
3782
3783        let plan = self.user_store.read(cx).plan();
3784        let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
3785
3786        plan.is_some_and(|plan| plan == Plan::ZedFree) && has_previous_trial
3787    }
3788
3789    fn should_render_onboarding(&self, cx: &mut Context<Self>) -> bool {
3790        if self.on_boarding_upsell_dismissed.load(Ordering::Acquire) {
3791            return false;
3792        }
3793
3794        let user_store = self.user_store.read(cx);
3795
3796        if user_store.plan().is_some_and(|plan| plan == Plan::ZedPro)
3797            && user_store
3798                .subscription_period()
3799                .and_then(|period| period.0.checked_add_days(chrono::Days::new(1)))
3800                .is_some_and(|date| date < chrono::Utc::now())
3801        {
3802            OnboardingUpsell::set_dismissed(true, cx);
3803            self.on_boarding_upsell_dismissed
3804                .store(true, Ordering::Release);
3805            return false;
3806        }
3807
3808        let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
3809            .visible_providers()
3810            .iter()
3811            .any(|provider| {
3812                provider.is_authenticated(cx)
3813                    && provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
3814            });
3815
3816        match &self.active_view {
3817            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {
3818                false
3819            }
3820            ActiveView::AgentThread {
3821                conversation_view, ..
3822            } if conversation_view.read(cx).as_native_thread(cx).is_none() => false,
3823            ActiveView::AgentThread { conversation_view } => {
3824                let history_is_empty = conversation_view
3825                    .read(cx)
3826                    .history()
3827                    .is_none_or(|h| h.read(cx).is_empty());
3828                history_is_empty || !has_configured_non_zed_providers
3829            }
3830        }
3831    }
3832
3833    fn render_onboarding(
3834        &self,
3835        _window: &mut Window,
3836        cx: &mut Context<Self>,
3837    ) -> Option<impl IntoElement> {
3838        if !self.should_render_onboarding(cx) {
3839            return None;
3840        }
3841
3842        Some(div().child(self.onboarding.clone()))
3843    }
3844
3845    fn render_trial_end_upsell(
3846        &self,
3847        _window: &mut Window,
3848        cx: &mut Context<Self>,
3849    ) -> Option<impl IntoElement> {
3850        if !self.should_render_trial_end_upsell(cx) {
3851            return None;
3852        }
3853
3854        Some(
3855            v_flex()
3856                .absolute()
3857                .inset_0()
3858                .size_full()
3859                .bg(cx.theme().colors().panel_background)
3860                .opacity(0.85)
3861                .block_mouse_except_scroll()
3862                .child(EndTrialUpsell::new(Arc::new({
3863                    let this = cx.entity();
3864                    move |_, cx| {
3865                        this.update(cx, |_this, cx| {
3866                            TrialEndUpsell::set_dismissed(true, cx);
3867                            cx.notify();
3868                        });
3869                    }
3870                }))),
3871        )
3872    }
3873
3874    fn render_drag_target(&self, cx: &Context<Self>) -> Div {
3875        let is_local = self.project.read(cx).is_local();
3876        div()
3877            .invisible()
3878            .absolute()
3879            .top_0()
3880            .right_0()
3881            .bottom_0()
3882            .left_0()
3883            .bg(cx.theme().colors().drop_target_background)
3884            .drag_over::<DraggedTab>(|this, _, _, _| this.visible())
3885            .drag_over::<DraggedSelection>(|this, _, _, _| this.visible())
3886            .when(is_local, |this| {
3887                this.drag_over::<ExternalPaths>(|this, _, _, _| this.visible())
3888            })
3889            .on_drop(cx.listener(move |this, tab: &DraggedTab, window, cx| {
3890                let item = tab.pane.read(cx).item_for_index(tab.ix);
3891                let project_paths = item
3892                    .and_then(|item| item.project_path(cx))
3893                    .into_iter()
3894                    .collect::<Vec<_>>();
3895                this.handle_drop(project_paths, vec![], window, cx);
3896            }))
3897            .on_drop(
3898                cx.listener(move |this, selection: &DraggedSelection, window, cx| {
3899                    let project_paths = selection
3900                        .items()
3901                        .filter_map(|item| this.project.read(cx).path_for_entry(item.entry_id, cx))
3902                        .collect::<Vec<_>>();
3903                    this.handle_drop(project_paths, vec![], window, cx);
3904                }),
3905            )
3906            .on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| {
3907                let tasks = paths
3908                    .paths()
3909                    .iter()
3910                    .map(|path| {
3911                        Workspace::project_path_for_path(this.project.clone(), path, false, cx)
3912                    })
3913                    .collect::<Vec<_>>();
3914                cx.spawn_in(window, async move |this, cx| {
3915                    let mut paths = vec![];
3916                    let mut added_worktrees = vec![];
3917                    let opened_paths = futures::future::join_all(tasks).await;
3918                    for entry in opened_paths {
3919                        if let Some((worktree, project_path)) = entry.log_err() {
3920                            added_worktrees.push(worktree);
3921                            paths.push(project_path);
3922                        }
3923                    }
3924                    this.update_in(cx, |this, window, cx| {
3925                        this.handle_drop(paths, added_worktrees, window, cx);
3926                    })
3927                    .ok();
3928                })
3929                .detach();
3930            }))
3931    }
3932
3933    fn handle_drop(
3934        &mut self,
3935        paths: Vec<ProjectPath>,
3936        added_worktrees: Vec<Entity<Worktree>>,
3937        window: &mut Window,
3938        cx: &mut Context<Self>,
3939    ) {
3940        match &self.active_view {
3941            ActiveView::AgentThread { conversation_view } => {
3942                conversation_view.update(cx, |conversation_view, cx| {
3943                    conversation_view.insert_dragged_files(paths, added_worktrees, window, cx);
3944                });
3945            }
3946            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {}
3947        }
3948    }
3949
3950    fn render_workspace_trust_message(&self, cx: &Context<Self>) -> Option<impl IntoElement> {
3951        if !self.show_trust_workspace_message {
3952            return None;
3953        }
3954
3955        let description = "To protect your system, third-party code—like MCP servers—won't run until you mark this workspace as safe.";
3956
3957        Some(
3958            Callout::new()
3959                .icon(IconName::Warning)
3960                .severity(Severity::Warning)
3961                .border_position(ui::BorderPosition::Bottom)
3962                .title("You're in Restricted Mode")
3963                .description(description)
3964                .actions_slot(
3965                    Button::new("open-trust-modal", "Configure Project Trust")
3966                        .label_size(LabelSize::Small)
3967                        .style(ButtonStyle::Outlined)
3968                        .on_click({
3969                            cx.listener(move |this, _, window, cx| {
3970                                this.workspace
3971                                    .update(cx, |workspace, cx| {
3972                                        workspace
3973                                            .show_worktree_trust_security_modal(true, window, cx)
3974                                    })
3975                                    .log_err();
3976                            })
3977                        }),
3978                ),
3979        )
3980    }
3981
3982    fn key_context(&self) -> KeyContext {
3983        let mut key_context = KeyContext::new_with_defaults();
3984        key_context.add("AgentPanel");
3985        match &self.active_view {
3986            ActiveView::AgentThread { .. } => key_context.add("acp_thread"),
3987            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {}
3988        }
3989        key_context
3990    }
3991}
3992
3993impl Render for AgentPanel {
3994    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3995        // WARNING: Changes to this element hierarchy can have
3996        // non-obvious implications to the layout of children.
3997        //
3998        // If you need to change it, please confirm:
3999        // - The message editor expands (cmd-option-esc) correctly
4000        // - When expanded, the buttons at the bottom of the panel are displayed correctly
4001        // - Font size works as expected and can be changed with cmd-+/cmd-
4002        // - Scrolling in all views works as expected
4003        // - Files can be dropped into the panel
4004        let content = v_flex()
4005            .relative()
4006            .size_full()
4007            .justify_between()
4008            .key_context(self.key_context())
4009            .on_action(cx.listener(|this, action: &NewThread, window, cx| {
4010                this.new_thread(action, window, cx);
4011            }))
4012            .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
4013                this.open_history(window, cx);
4014            }))
4015            .on_action(cx.listener(|this, _: &OpenSettings, window, cx| {
4016                this.open_configuration(window, cx);
4017            }))
4018            .on_action(cx.listener(Self::open_active_thread_as_markdown))
4019            .on_action(cx.listener(Self::deploy_rules_library))
4020            .on_action(cx.listener(Self::go_back))
4021            .on_action(cx.listener(Self::toggle_navigation_menu))
4022            .on_action(cx.listener(Self::toggle_options_menu))
4023            .on_action(cx.listener(Self::increase_font_size))
4024            .on_action(cx.listener(Self::decrease_font_size))
4025            .on_action(cx.listener(Self::reset_font_size))
4026            .on_action(cx.listener(Self::toggle_zoom))
4027            .on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| {
4028                if let Some(conversation_view) = this.active_conversation_view() {
4029                    conversation_view.update(cx, |conversation_view, cx| {
4030                        conversation_view.reauthenticate(window, cx)
4031                    })
4032                }
4033            }))
4034            .child(self.render_toolbar(window, cx))
4035            .children(self.render_workspace_trust_message(cx))
4036            .children(self.render_onboarding(window, cx))
4037            .map(|parent| match &self.active_view {
4038                ActiveView::Uninitialized => parent,
4039                ActiveView::AgentThread {
4040                    conversation_view, ..
4041                } => parent
4042                    .child(conversation_view.clone())
4043                    .child(self.render_drag_target(cx)),
4044                ActiveView::History { view } => parent.child(view.clone()),
4045                ActiveView::Configuration => parent.children(self.configuration.clone()),
4046            })
4047            .children(self.render_worktree_creation_status(cx))
4048            .children(self.render_trial_end_upsell(window, cx));
4049
4050        match self.active_view.which_font_size_used() {
4051            WhichFontSize::AgentFont => {
4052                WithRemSize::new(ThemeSettings::get_global(cx).agent_ui_font_size(cx))
4053                    .size_full()
4054                    .child(content)
4055                    .into_any()
4056            }
4057            _ => content.into_any(),
4058        }
4059    }
4060}
4061
4062struct PromptLibraryInlineAssist {
4063    workspace: WeakEntity<Workspace>,
4064}
4065
4066impl PromptLibraryInlineAssist {
4067    pub fn new(workspace: WeakEntity<Workspace>) -> Self {
4068        Self { workspace }
4069    }
4070}
4071
4072impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
4073    fn assist(
4074        &self,
4075        prompt_editor: &Entity<Editor>,
4076        initial_prompt: Option<String>,
4077        window: &mut Window,
4078        cx: &mut Context<RulesLibrary>,
4079    ) {
4080        InlineAssistant::update_global(cx, |assistant, cx| {
4081            let Some(workspace) = self.workspace.upgrade() else {
4082                return;
4083            };
4084            let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) else {
4085                return;
4086            };
4087            let history = panel
4088                .read(cx)
4089                .connection_store()
4090                .read(cx)
4091                .entry(&crate::Agent::NativeAgent)
4092                .and_then(|s| s.read(cx).history())
4093                .map(|h| h.downgrade());
4094            let project = workspace.read(cx).project().downgrade();
4095            let panel = panel.read(cx);
4096            let thread_store = panel.thread_store().clone();
4097            assistant.assist(
4098                prompt_editor,
4099                self.workspace.clone(),
4100                project,
4101                thread_store,
4102                None,
4103                history,
4104                initial_prompt,
4105                window,
4106                cx,
4107            );
4108        })
4109    }
4110
4111    fn focus_agent_panel(
4112        &self,
4113        workspace: &mut Workspace,
4114        window: &mut Window,
4115        cx: &mut Context<Workspace>,
4116    ) -> bool {
4117        workspace.focus_panel::<AgentPanel>(window, cx).is_some()
4118    }
4119}
4120
4121struct OnboardingUpsell;
4122
4123impl Dismissable for OnboardingUpsell {
4124    const KEY: &'static str = "dismissed-trial-upsell";
4125}
4126
4127struct TrialEndUpsell;
4128
4129impl Dismissable for TrialEndUpsell {
4130    const KEY: &'static str = "dismissed-trial-end-upsell";
4131}
4132
4133/// Test-only helper methods
4134#[cfg(any(test, feature = "test-support"))]
4135impl AgentPanel {
4136    pub fn test_new(workspace: &Workspace, window: &mut Window, cx: &mut Context<Self>) -> Self {
4137        Self::new(workspace, None, window, cx)
4138    }
4139
4140    /// Opens an external thread using an arbitrary AgentServer.
4141    ///
4142    /// This is a test-only helper that allows visual tests and integration tests
4143    /// to inject a stub server without modifying production code paths.
4144    /// Not compiled into production builds.
4145    pub fn open_external_thread_with_server(
4146        &mut self,
4147        server: Rc<dyn AgentServer>,
4148        window: &mut Window,
4149        cx: &mut Context<Self>,
4150    ) {
4151        let workspace = self.workspace.clone();
4152        let project = self.project.clone();
4153
4154        let ext_agent = Agent::Custom {
4155            id: server.agent_id(),
4156        };
4157
4158        self.create_agent_thread(
4159            server, None, None, None, None, workspace, project, ext_agent, true, window, cx,
4160        );
4161    }
4162
4163    /// Returns the currently active thread view, if any.
4164    ///
4165    /// This is a test-only accessor that exposes the private `active_thread_view()`
4166    /// method for test assertions. Not compiled into production builds.
4167    pub fn active_thread_view_for_tests(&self) -> Option<&Entity<ConversationView>> {
4168        self.active_conversation_view()
4169    }
4170
4171    /// Sets the start_thread_in value directly, bypassing validation.
4172    ///
4173    /// This is a test-only helper for visual tests that need to show specific
4174    /// start_thread_in states without requiring a real git repository.
4175    pub fn set_start_thread_in_for_tests(&mut self, target: StartThreadIn, cx: &mut Context<Self>) {
4176        self.start_thread_in = target;
4177        cx.notify();
4178    }
4179
4180    /// Returns the current worktree creation status.
4181    ///
4182    /// This is a test-only helper for visual tests.
4183    pub fn worktree_creation_status_for_tests(&self) -> Option<&WorktreeCreationStatus> {
4184        self.worktree_creation_status.as_ref()
4185    }
4186
4187    /// Sets the worktree creation status directly.
4188    ///
4189    /// This is a test-only helper for visual tests that need to show the
4190    /// "Creating worktree…" spinner or error banners.
4191    pub fn set_worktree_creation_status_for_tests(
4192        &mut self,
4193        status: Option<WorktreeCreationStatus>,
4194        cx: &mut Context<Self>,
4195    ) {
4196        self.worktree_creation_status = status;
4197        cx.notify();
4198    }
4199
4200    /// Opens the history view.
4201    ///
4202    /// This is a test-only helper that exposes the private `open_history()`
4203    /// method for visual tests.
4204    pub fn open_history_for_tests(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4205        self.open_history(window, cx);
4206    }
4207
4208    /// Opens the start_thread_in selector popover menu.
4209    ///
4210    /// This is a test-only helper for visual tests.
4211    pub fn open_start_thread_in_menu_for_tests(
4212        &mut self,
4213        window: &mut Window,
4214        cx: &mut Context<Self>,
4215    ) {
4216        self.start_thread_in_menu_handle.show(window, cx);
4217    }
4218
4219    /// Dismisses the start_thread_in dropdown menu.
4220    ///
4221    /// This is a test-only helper for visual tests.
4222    pub fn close_start_thread_in_menu_for_tests(&mut self, cx: &mut Context<Self>) {
4223        self.start_thread_in_menu_handle.hide(cx);
4224    }
4225}
4226
4227#[cfg(test)]
4228mod tests {
4229    use super::*;
4230    use crate::conversation_view::tests::{StubAgentServer, init_test};
4231    use crate::test_support::{
4232        active_session_id, open_thread_with_connection, open_thread_with_custom_connection,
4233        send_message,
4234    };
4235    use acp_thread::{StubAgentConnection, ThreadStatus};
4236    use agent_servers::CODEX_ID;
4237    use feature_flags::FeatureFlagAppExt;
4238    use fs::FakeFs;
4239    use gpui::{TestAppContext, VisualTestContext};
4240    use project::Project;
4241    use serde_json::json;
4242    use std::path::Path;
4243    use std::time::Instant;
4244    use workspace::MultiWorkspace;
4245
4246    #[gpui::test]
4247    async fn test_active_thread_serialize_and_load_round_trip(cx: &mut TestAppContext) {
4248        init_test(cx);
4249        cx.update(|cx| {
4250            cx.update_flags(true, vec!["agent-v2".to_string()]);
4251            agent::ThreadStore::init_global(cx);
4252            language_model::LanguageModelRegistry::test(cx);
4253        });
4254
4255        // --- Create a MultiWorkspace window with two workspaces ---
4256        let fs = FakeFs::new(cx.executor());
4257        let project_a = Project::test(fs.clone(), [], cx).await;
4258        let project_b = Project::test(fs, [], cx).await;
4259
4260        let multi_workspace =
4261            cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
4262
4263        let workspace_a = multi_workspace
4264            .read_with(cx, |multi_workspace, _cx| {
4265                multi_workspace.workspace().clone()
4266            })
4267            .unwrap();
4268
4269        let workspace_b = multi_workspace
4270            .update(cx, |multi_workspace, window, cx| {
4271                multi_workspace.test_add_workspace(project_b.clone(), window, cx)
4272            })
4273            .unwrap();
4274
4275        workspace_a.update(cx, |workspace, _cx| {
4276            workspace.set_random_database_id();
4277        });
4278        workspace_b.update(cx, |workspace, _cx| {
4279            workspace.set_random_database_id();
4280        });
4281
4282        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
4283
4284        // --- Set up workspace A: with an active thread ---
4285        let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
4286            cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
4287        });
4288
4289        panel_a.update_in(cx, |panel, window, cx| {
4290            panel.open_external_thread_with_server(
4291                Rc::new(StubAgentServer::default_response()),
4292                window,
4293                cx,
4294            );
4295        });
4296
4297        cx.run_until_parked();
4298
4299        panel_a.read_with(cx, |panel, cx| {
4300            assert!(
4301                panel.active_agent_thread(cx).is_some(),
4302                "workspace A should have an active thread after connection"
4303            );
4304        });
4305
4306        send_message(&panel_a, cx);
4307
4308        let agent_type_a = panel_a.read_with(cx, |panel, _cx| panel.selected_agent.clone());
4309
4310        // --- Set up workspace B: ClaudeCode, no active thread ---
4311        let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
4312            cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
4313        });
4314
4315        panel_b.update(cx, |panel, _cx| {
4316            panel.selected_agent = Agent::Custom {
4317                id: "claude-acp".into(),
4318            };
4319        });
4320
4321        // --- Serialize both panels ---
4322        panel_a.update(cx, |panel, cx| panel.serialize(cx));
4323        panel_b.update(cx, |panel, cx| panel.serialize(cx));
4324        cx.run_until_parked();
4325
4326        // --- Load fresh panels for each workspace and verify independent state ---
4327        let async_cx = cx.update(|window, cx| window.to_async(cx));
4328        let loaded_a = AgentPanel::load(workspace_a.downgrade(), async_cx)
4329            .await
4330            .expect("panel A load should succeed");
4331        cx.run_until_parked();
4332
4333        let async_cx = cx.update(|window, cx| window.to_async(cx));
4334        let loaded_b = AgentPanel::load(workspace_b.downgrade(), async_cx)
4335            .await
4336            .expect("panel B load should succeed");
4337        cx.run_until_parked();
4338
4339        // Workspace A should restore its thread and agent type
4340        loaded_a.read_with(cx, |panel, _cx| {
4341            assert_eq!(
4342                panel.selected_agent, agent_type_a,
4343                "workspace A agent type should be restored"
4344            );
4345            assert!(
4346                panel.active_conversation_view().is_some(),
4347                "workspace A should have its active thread restored"
4348            );
4349        });
4350
4351        // Workspace B should restore its own agent type, with no thread
4352        loaded_b.read_with(cx, |panel, _cx| {
4353            assert_eq!(
4354                panel.selected_agent,
4355                Agent::Custom {
4356                    id: "claude-acp".into()
4357                },
4358                "workspace B agent type should be restored"
4359            );
4360            assert!(
4361                panel.active_conversation_view().is_none(),
4362                "workspace B should have no active thread"
4363            );
4364        });
4365    }
4366
4367    #[gpui::test]
4368    async fn test_non_native_thread_without_metadata_is_not_restored(cx: &mut TestAppContext) {
4369        init_test(cx);
4370        cx.update(|cx| {
4371            cx.update_flags(true, vec!["agent-v2".to_string()]);
4372            agent::ThreadStore::init_global(cx);
4373            language_model::LanguageModelRegistry::test(cx);
4374        });
4375
4376        let fs = FakeFs::new(cx.executor());
4377        let project = Project::test(fs, [], cx).await;
4378
4379        let multi_workspace =
4380            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4381
4382        let workspace = multi_workspace
4383            .read_with(cx, |multi_workspace, _cx| {
4384                multi_workspace.workspace().clone()
4385            })
4386            .unwrap();
4387
4388        workspace.update(cx, |workspace, _cx| {
4389            workspace.set_random_database_id();
4390        });
4391
4392        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
4393
4394        let panel = workspace.update_in(cx, |workspace, window, cx| {
4395            cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
4396        });
4397
4398        panel.update_in(cx, |panel, window, cx| {
4399            panel.open_external_thread_with_server(
4400                Rc::new(StubAgentServer::default_response()),
4401                window,
4402                cx,
4403            );
4404        });
4405
4406        cx.run_until_parked();
4407
4408        panel.read_with(cx, |panel, cx| {
4409            assert!(
4410                panel.active_agent_thread(cx).is_some(),
4411                "should have an active thread after connection"
4412            );
4413        });
4414
4415        // Serialize without ever sending a message, so no thread metadata exists.
4416        panel.update(cx, |panel, cx| panel.serialize(cx));
4417        cx.run_until_parked();
4418
4419        let async_cx = cx.update(|window, cx| window.to_async(cx));
4420        let loaded = AgentPanel::load(workspace.downgrade(), async_cx)
4421            .await
4422            .expect("panel load should succeed");
4423        cx.run_until_parked();
4424
4425        loaded.read_with(cx, |panel, _cx| {
4426            assert!(
4427                panel.active_conversation_view().is_none(),
4428                "thread without metadata should not be restored"
4429            );
4430        });
4431    }
4432
4433    /// Extracts the text from a Text content block, panicking if it's not Text.
4434    fn expect_text_block(block: &acp::ContentBlock) -> &str {
4435        match block {
4436            acp::ContentBlock::Text(t) => t.text.as_str(),
4437            other => panic!("expected Text block, got {:?}", other),
4438        }
4439    }
4440
4441    /// Extracts the (text_content, uri) from a Resource content block, panicking
4442    /// if it's not a TextResourceContents resource.
4443    fn expect_resource_block(block: &acp::ContentBlock) -> (&str, &str) {
4444        match block {
4445            acp::ContentBlock::Resource(r) => match &r.resource {
4446                acp::EmbeddedResourceResource::TextResourceContents(t) => {
4447                    (t.text.as_str(), t.uri.as_str())
4448                }
4449                other => panic!("expected TextResourceContents, got {:?}", other),
4450            },
4451            other => panic!("expected Resource block, got {:?}", other),
4452        }
4453    }
4454
4455    #[test]
4456    fn test_build_conflict_resolution_prompt_single_conflict() {
4457        let conflicts = vec![ConflictContent {
4458            file_path: "src/main.rs".to_string(),
4459            conflict_text: "<<<<<<< HEAD\nlet x = 1;\n=======\nlet x = 2;\n>>>>>>> feature"
4460                .to_string(),
4461            ours_branch_name: "HEAD".to_string(),
4462            theirs_branch_name: "feature".to_string(),
4463        }];
4464
4465        let blocks = build_conflict_resolution_prompt(&conflicts);
4466        // 2 Text blocks + 1 ResourceLink + 1 Resource for the conflict
4467        assert_eq!(
4468            blocks.len(),
4469            4,
4470            "expected 2 text + 1 resource link + 1 resource block"
4471        );
4472
4473        let intro_text = expect_text_block(&blocks[0]);
4474        assert!(
4475            intro_text.contains("Please resolve the following merge conflict in"),
4476            "prompt should include single-conflict intro text"
4477        );
4478
4479        match &blocks[1] {
4480            acp::ContentBlock::ResourceLink(link) => {
4481                assert!(
4482                    link.uri.contains("file://"),
4483                    "resource link URI should use file scheme"
4484                );
4485                assert!(
4486                    link.uri.contains("main.rs"),
4487                    "resource link URI should reference file path"
4488                );
4489            }
4490            other => panic!("expected ResourceLink block, got {:?}", other),
4491        }
4492
4493        let body_text = expect_text_block(&blocks[2]);
4494        assert!(
4495            body_text.contains("`HEAD` (ours)"),
4496            "prompt should mention ours branch"
4497        );
4498        assert!(
4499            body_text.contains("`feature` (theirs)"),
4500            "prompt should mention theirs branch"
4501        );
4502        assert!(
4503            body_text.contains("editing the file directly"),
4504            "prompt should instruct the agent to edit the file"
4505        );
4506
4507        let (resource_text, resource_uri) = expect_resource_block(&blocks[3]);
4508        assert!(
4509            resource_text.contains("<<<<<<< HEAD"),
4510            "resource should contain the conflict text"
4511        );
4512        assert!(
4513            resource_uri.contains("merge-conflict"),
4514            "resource URI should use the merge-conflict scheme"
4515        );
4516        assert!(
4517            resource_uri.contains("main.rs"),
4518            "resource URI should reference the file path"
4519        );
4520    }
4521
4522    #[test]
4523    fn test_build_conflict_resolution_prompt_multiple_conflicts_same_file() {
4524        let conflicts = vec![
4525            ConflictContent {
4526                file_path: "src/lib.rs".to_string(),
4527                conflict_text: "<<<<<<< main\nfn a() {}\n=======\nfn a_v2() {}\n>>>>>>> dev"
4528                    .to_string(),
4529                ours_branch_name: "main".to_string(),
4530                theirs_branch_name: "dev".to_string(),
4531            },
4532            ConflictContent {
4533                file_path: "src/lib.rs".to_string(),
4534                conflict_text: "<<<<<<< main\nfn b() {}\n=======\nfn b_v2() {}\n>>>>>>> dev"
4535                    .to_string(),
4536                ours_branch_name: "main".to_string(),
4537                theirs_branch_name: "dev".to_string(),
4538            },
4539        ];
4540
4541        let blocks = build_conflict_resolution_prompt(&conflicts);
4542        // 1 Text instruction + 2 Resource blocks
4543        assert_eq!(blocks.len(), 3, "expected 1 text + 2 resource blocks");
4544
4545        let text = expect_text_block(&blocks[0]);
4546        assert!(
4547            text.contains("all 2 merge conflicts"),
4548            "prompt should mention the total count"
4549        );
4550        assert!(
4551            text.contains("`main` (ours)"),
4552            "prompt should mention ours branch"
4553        );
4554        assert!(
4555            text.contains("`dev` (theirs)"),
4556            "prompt should mention theirs branch"
4557        );
4558        // Single file, so "file" not "files"
4559        assert!(
4560            text.contains("file directly"),
4561            "single file should use singular 'file'"
4562        );
4563
4564        let (resource_a, _) = expect_resource_block(&blocks[1]);
4565        let (resource_b, _) = expect_resource_block(&blocks[2]);
4566        assert!(
4567            resource_a.contains("fn a()"),
4568            "first resource should contain first conflict"
4569        );
4570        assert!(
4571            resource_b.contains("fn b()"),
4572            "second resource should contain second conflict"
4573        );
4574    }
4575
4576    #[test]
4577    fn test_build_conflict_resolution_prompt_multiple_conflicts_different_files() {
4578        let conflicts = vec![
4579            ConflictContent {
4580                file_path: "src/a.rs".to_string(),
4581                conflict_text: "<<<<<<< main\nA\n=======\nB\n>>>>>>> dev".to_string(),
4582                ours_branch_name: "main".to_string(),
4583                theirs_branch_name: "dev".to_string(),
4584            },
4585            ConflictContent {
4586                file_path: "src/b.rs".to_string(),
4587                conflict_text: "<<<<<<< main\nC\n=======\nD\n>>>>>>> dev".to_string(),
4588                ours_branch_name: "main".to_string(),
4589                theirs_branch_name: "dev".to_string(),
4590            },
4591        ];
4592
4593        let blocks = build_conflict_resolution_prompt(&conflicts);
4594        // 1 Text instruction + 2 Resource blocks
4595        assert_eq!(blocks.len(), 3, "expected 1 text + 2 resource blocks");
4596
4597        let text = expect_text_block(&blocks[0]);
4598        assert!(
4599            text.contains("files directly"),
4600            "multiple files should use plural 'files'"
4601        );
4602
4603        let (_, uri_a) = expect_resource_block(&blocks[1]);
4604        let (_, uri_b) = expect_resource_block(&blocks[2]);
4605        assert!(
4606            uri_a.contains("a.rs"),
4607            "first resource URI should reference a.rs"
4608        );
4609        assert!(
4610            uri_b.contains("b.rs"),
4611            "second resource URI should reference b.rs"
4612        );
4613    }
4614
4615    #[test]
4616    fn test_build_conflicted_files_resolution_prompt_file_paths_only() {
4617        let file_paths = vec![
4618            "src/main.rs".to_string(),
4619            "src/lib.rs".to_string(),
4620            "tests/integration.rs".to_string(),
4621        ];
4622
4623        let blocks = build_conflicted_files_resolution_prompt(&file_paths);
4624        // 1 instruction Text block + (ResourceLink + newline Text) per file
4625        assert_eq!(
4626            blocks.len(),
4627            1 + (file_paths.len() * 2),
4628            "expected instruction text plus resource links and separators"
4629        );
4630
4631        let text = expect_text_block(&blocks[0]);
4632        assert!(
4633            text.contains("unresolved merge conflicts"),
4634            "prompt should describe the task"
4635        );
4636        assert!(
4637            text.contains("conflict markers"),
4638            "prompt should mention conflict markers"
4639        );
4640
4641        for (index, path) in file_paths.iter().enumerate() {
4642            let link_index = 1 + (index * 2);
4643            let newline_index = link_index + 1;
4644
4645            match &blocks[link_index] {
4646                acp::ContentBlock::ResourceLink(link) => {
4647                    assert!(
4648                        link.uri.contains("file://"),
4649                        "resource link URI should use file scheme"
4650                    );
4651                    assert!(
4652                        link.uri.contains(path),
4653                        "resource link URI should reference file path: {path}"
4654                    );
4655                }
4656                other => panic!(
4657                    "expected ResourceLink block at index {}, got {:?}",
4658                    link_index, other
4659                ),
4660            }
4661
4662            let separator = expect_text_block(&blocks[newline_index]);
4663            assert_eq!(
4664                separator, "\n",
4665                "expected newline separator after each file"
4666            );
4667        }
4668    }
4669
4670    #[test]
4671    fn test_build_conflict_resolution_prompt_empty_conflicts() {
4672        let blocks = build_conflict_resolution_prompt(&[]);
4673        assert!(
4674            blocks.is_empty(),
4675            "empty conflicts should produce no blocks, got {} blocks",
4676            blocks.len()
4677        );
4678    }
4679
4680    #[test]
4681    fn test_build_conflicted_files_resolution_prompt_empty_paths() {
4682        let blocks = build_conflicted_files_resolution_prompt(&[]);
4683        assert!(
4684            blocks.is_empty(),
4685            "empty paths should produce no blocks, got {} blocks",
4686            blocks.len()
4687        );
4688    }
4689
4690    #[test]
4691    fn test_conflict_resource_block_structure() {
4692        let conflict = ConflictContent {
4693            file_path: "src/utils.rs".to_string(),
4694            conflict_text: "<<<<<<< HEAD\nold code\n=======\nnew code\n>>>>>>> branch".to_string(),
4695            ours_branch_name: "HEAD".to_string(),
4696            theirs_branch_name: "branch".to_string(),
4697        };
4698
4699        let block = conflict_resource_block(&conflict);
4700        let (text, uri) = expect_resource_block(&block);
4701
4702        assert_eq!(
4703            text, conflict.conflict_text,
4704            "resource text should be the raw conflict"
4705        );
4706        assert!(
4707            uri.starts_with("zed:///agent/merge-conflict"),
4708            "URI should use the zed merge-conflict scheme, got: {uri}"
4709        );
4710        assert!(uri.contains("utils.rs"), "URI should encode the file path");
4711    }
4712
4713    fn open_generating_thread_with_loadable_connection(
4714        panel: &Entity<AgentPanel>,
4715        connection: &StubAgentConnection,
4716        cx: &mut VisualTestContext,
4717    ) -> acp::SessionId {
4718        open_thread_with_custom_connection(panel, connection.clone(), cx);
4719        let session_id = active_session_id(panel, cx);
4720        send_message(panel, cx);
4721        cx.update(|_, cx| {
4722            connection.send_update(
4723                session_id.clone(),
4724                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("done".into())),
4725                cx,
4726            );
4727        });
4728        cx.run_until_parked();
4729        session_id
4730    }
4731
4732    fn open_idle_thread_with_non_loadable_connection(
4733        panel: &Entity<AgentPanel>,
4734        connection: &StubAgentConnection,
4735        cx: &mut VisualTestContext,
4736    ) -> acp::SessionId {
4737        open_thread_with_custom_connection(panel, connection.clone(), cx);
4738        let session_id = active_session_id(panel, cx);
4739
4740        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
4741            acp::ContentChunk::new("done".into()),
4742        )]);
4743        send_message(panel, cx);
4744
4745        session_id
4746    }
4747
4748    async fn setup_panel(cx: &mut TestAppContext) -> (Entity<AgentPanel>, VisualTestContext) {
4749        init_test(cx);
4750        cx.update(|cx| {
4751            cx.update_flags(true, vec!["agent-v2".to_string()]);
4752            agent::ThreadStore::init_global(cx);
4753            language_model::LanguageModelRegistry::test(cx);
4754        });
4755
4756        let fs = FakeFs::new(cx.executor());
4757        let project = Project::test(fs.clone(), [], cx).await;
4758
4759        let multi_workspace =
4760            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4761
4762        let workspace = multi_workspace
4763            .read_with(cx, |mw, _cx| mw.workspace().clone())
4764            .unwrap();
4765
4766        let mut cx = VisualTestContext::from_window(multi_workspace.into(), cx);
4767
4768        let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
4769            cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
4770        });
4771
4772        (panel, cx)
4773    }
4774
4775    #[gpui::test]
4776    async fn test_empty_draft_thread_not_retained_when_navigating_away(cx: &mut TestAppContext) {
4777        let (panel, mut cx) = setup_panel(cx).await;
4778
4779        let connection_a = StubAgentConnection::new();
4780        open_thread_with_connection(&panel, connection_a, &mut cx);
4781        let session_id_a = active_session_id(&panel, &cx);
4782
4783        panel.read_with(&cx, |panel, cx| {
4784            let thread = panel.active_agent_thread(cx).unwrap();
4785            assert!(
4786                thread.read(cx).entries().is_empty(),
4787                "newly opened draft thread should have no entries"
4788            );
4789            assert!(panel.background_threads.is_empty());
4790        });
4791
4792        let connection_b = StubAgentConnection::new();
4793        open_thread_with_connection(&panel, connection_b, &mut cx);
4794
4795        panel.read_with(&cx, |panel, _cx| {
4796            assert!(
4797                panel.background_threads.is_empty(),
4798                "empty draft thread should not be retained in background_threads"
4799            );
4800            assert!(
4801                !panel.background_threads.contains_key(&session_id_a),
4802                "empty draft thread should not be keyed in background_threads"
4803            );
4804        });
4805    }
4806
4807    #[gpui::test]
4808    async fn test_running_thread_retained_when_navigating_away(cx: &mut TestAppContext) {
4809        let (panel, mut cx) = setup_panel(cx).await;
4810
4811        let connection_a = StubAgentConnection::new();
4812        open_thread_with_connection(&panel, connection_a.clone(), &mut cx);
4813        send_message(&panel, &mut cx);
4814
4815        let session_id_a = active_session_id(&panel, &cx);
4816
4817        // Send a chunk to keep thread A generating (don't end the turn).
4818        cx.update(|_, cx| {
4819            connection_a.send_update(
4820                session_id_a.clone(),
4821                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("chunk".into())),
4822                cx,
4823            );
4824        });
4825        cx.run_until_parked();
4826
4827        // Verify thread A is generating.
4828        panel.read_with(&cx, |panel, cx| {
4829            let thread = panel.active_agent_thread(cx).unwrap();
4830            assert_eq!(thread.read(cx).status(), ThreadStatus::Generating);
4831            assert!(panel.background_threads.is_empty());
4832        });
4833
4834        // Open a new thread B — thread A should be retained in background.
4835        let connection_b = StubAgentConnection::new();
4836        open_thread_with_connection(&panel, connection_b, &mut cx);
4837
4838        panel.read_with(&cx, |panel, _cx| {
4839            assert_eq!(
4840                panel.background_threads.len(),
4841                1,
4842                "Running thread A should be retained in background_views"
4843            );
4844            assert!(
4845                panel.background_threads.contains_key(&session_id_a),
4846                "Background view should be keyed by thread A's session ID"
4847            );
4848        });
4849    }
4850
4851    #[gpui::test]
4852    async fn test_idle_non_loadable_thread_retained_when_navigating_away(cx: &mut TestAppContext) {
4853        let (panel, mut cx) = setup_panel(cx).await;
4854
4855        let connection_a = StubAgentConnection::new();
4856        connection_a.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
4857            acp::ContentChunk::new("Response".into()),
4858        )]);
4859        open_thread_with_connection(&panel, connection_a, &mut cx);
4860        send_message(&panel, &mut cx);
4861
4862        let weak_view_a = panel.read_with(&cx, |panel, _cx| {
4863            panel.active_conversation_view().unwrap().downgrade()
4864        });
4865        let session_id_a = active_session_id(&panel, &cx);
4866
4867        // Thread A should be idle (auto-completed via set_next_prompt_updates).
4868        panel.read_with(&cx, |panel, cx| {
4869            let thread = panel.active_agent_thread(cx).unwrap();
4870            assert_eq!(thread.read(cx).status(), ThreadStatus::Idle);
4871        });
4872
4873        // Open a new thread B — thread A should be retained because it is not loadable.
4874        let connection_b = StubAgentConnection::new();
4875        open_thread_with_connection(&panel, connection_b, &mut cx);
4876
4877        panel.read_with(&cx, |panel, _cx| {
4878            assert_eq!(
4879                panel.background_threads.len(),
4880                1,
4881                "Idle non-loadable thread A should be retained in background_views"
4882            );
4883            assert!(
4884                panel.background_threads.contains_key(&session_id_a),
4885                "Background view should be keyed by thread A's session ID"
4886            );
4887        });
4888
4889        assert!(
4890            weak_view_a.upgrade().is_some(),
4891            "Idle non-loadable ConnectionView should still be retained"
4892        );
4893    }
4894
4895    #[gpui::test]
4896    async fn test_background_thread_promoted_via_load(cx: &mut TestAppContext) {
4897        let (panel, mut cx) = setup_panel(cx).await;
4898
4899        let connection_a = StubAgentConnection::new();
4900        open_thread_with_connection(&panel, connection_a.clone(), &mut cx);
4901        send_message(&panel, &mut cx);
4902
4903        let session_id_a = active_session_id(&panel, &cx);
4904
4905        // Keep thread A generating.
4906        cx.update(|_, cx| {
4907            connection_a.send_update(
4908                session_id_a.clone(),
4909                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("chunk".into())),
4910                cx,
4911            );
4912        });
4913        cx.run_until_parked();
4914
4915        // Open thread B — thread A goes to background.
4916        let connection_b = StubAgentConnection::new();
4917        open_thread_with_connection(&panel, connection_b, &mut cx);
4918        send_message(&panel, &mut cx);
4919
4920        let session_id_b = active_session_id(&panel, &cx);
4921
4922        panel.read_with(&cx, |panel, _cx| {
4923            assert_eq!(panel.background_threads.len(), 1);
4924            assert!(panel.background_threads.contains_key(&session_id_a));
4925        });
4926
4927        // Load thread A back via load_agent_thread — should promote from background.
4928        panel.update_in(&mut cx, |panel, window, cx| {
4929            panel.load_agent_thread(
4930                panel.selected_agent().expect("selected agent must be set"),
4931                session_id_a.clone(),
4932                None,
4933                None,
4934                true,
4935                window,
4936                cx,
4937            );
4938        });
4939
4940        // Thread A should now be the active view, promoted from background.
4941        let active_session = active_session_id(&panel, &cx);
4942        assert_eq!(
4943            active_session, session_id_a,
4944            "Thread A should be the active thread after promotion"
4945        );
4946
4947        panel.read_with(&cx, |panel, _cx| {
4948            assert!(
4949                !panel.background_threads.contains_key(&session_id_a),
4950                "Promoted thread A should no longer be in background_views"
4951            );
4952            assert!(
4953                panel.background_threads.contains_key(&session_id_b),
4954                "Thread B (idle, non-loadable) should remain retained in background_views"
4955            );
4956        });
4957    }
4958
4959    #[gpui::test]
4960    async fn test_cleanup_background_threads_keeps_five_most_recent_idle_loadable_threads(
4961        cx: &mut TestAppContext,
4962    ) {
4963        let (panel, mut cx) = setup_panel(cx).await;
4964        let connection = StubAgentConnection::new()
4965            .with_supports_load_session(true)
4966            .with_agent_id("loadable-stub".into())
4967            .with_telemetry_id("loadable-stub".into());
4968        let mut session_ids = Vec::new();
4969
4970        for _ in 0..7 {
4971            session_ids.push(open_generating_thread_with_loadable_connection(
4972                &panel,
4973                &connection,
4974                &mut cx,
4975            ));
4976        }
4977
4978        let base_time = Instant::now();
4979
4980        for session_id in session_ids.iter().take(6) {
4981            connection.end_turn(session_id.clone(), acp::StopReason::EndTurn);
4982        }
4983        cx.run_until_parked();
4984
4985        panel.update(&mut cx, |panel, cx| {
4986            for (index, session_id) in session_ids.iter().take(6).enumerate() {
4987                let conversation_view = panel
4988                    .background_threads
4989                    .get(session_id)
4990                    .expect("background thread should exist")
4991                    .clone();
4992                conversation_view.update(cx, |view, cx| {
4993                    view.set_updated_at(base_time + Duration::from_secs(index as u64), cx);
4994                });
4995            }
4996            panel.cleanup_background_threads(cx);
4997        });
4998
4999        panel.read_with(&cx, |panel, _cx| {
5000            assert_eq!(
5001                panel.background_threads.len(),
5002                5,
5003                "cleanup should keep at most five idle loadable background threads"
5004            );
5005            assert!(
5006                !panel.background_threads.contains_key(&session_ids[0]),
5007                "oldest idle loadable background thread should be removed"
5008            );
5009            for session_id in &session_ids[1..6] {
5010                assert!(
5011                    panel.background_threads.contains_key(session_id),
5012                    "more recent idle loadable background threads should be retained"
5013                );
5014            }
5015            assert!(
5016                !panel.background_threads.contains_key(&session_ids[6]),
5017                "the active thread should not also be stored as a background thread"
5018            );
5019        });
5020    }
5021
5022    #[gpui::test]
5023    async fn test_cleanup_background_threads_preserves_idle_non_loadable_threads(
5024        cx: &mut TestAppContext,
5025    ) {
5026        let (panel, mut cx) = setup_panel(cx).await;
5027
5028        let non_loadable_connection = StubAgentConnection::new();
5029        let non_loadable_session_id = open_idle_thread_with_non_loadable_connection(
5030            &panel,
5031            &non_loadable_connection,
5032            &mut cx,
5033        );
5034
5035        let loadable_connection = StubAgentConnection::new()
5036            .with_supports_load_session(true)
5037            .with_agent_id("loadable-stub".into())
5038            .with_telemetry_id("loadable-stub".into());
5039        let mut loadable_session_ids = Vec::new();
5040
5041        for _ in 0..7 {
5042            loadable_session_ids.push(open_generating_thread_with_loadable_connection(
5043                &panel,
5044                &loadable_connection,
5045                &mut cx,
5046            ));
5047        }
5048
5049        let base_time = Instant::now();
5050
5051        for session_id in loadable_session_ids.iter().take(6) {
5052            loadable_connection.end_turn(session_id.clone(), acp::StopReason::EndTurn);
5053        }
5054        cx.run_until_parked();
5055
5056        panel.update(&mut cx, |panel, cx| {
5057            for (index, session_id) in loadable_session_ids.iter().take(6).enumerate() {
5058                let conversation_view = panel
5059                    .background_threads
5060                    .get(session_id)
5061                    .expect("background thread should exist")
5062                    .clone();
5063                conversation_view.update(cx, |view, cx| {
5064                    view.set_updated_at(base_time + Duration::from_secs(index as u64), cx);
5065                });
5066            }
5067            panel.cleanup_background_threads(cx);
5068        });
5069
5070        panel.read_with(&cx, |panel, _cx| {
5071            assert_eq!(
5072                panel.background_threads.len(),
5073                6,
5074                "cleanup should keep the non-loadable idle thread in addition to five loadable ones"
5075            );
5076            assert!(
5077                panel
5078                    .background_threads
5079                    .contains_key(&non_loadable_session_id),
5080                "idle non-loadable background threads should not be cleanup candidates"
5081            );
5082            assert!(
5083                !panel
5084                    .background_threads
5085                    .contains_key(&loadable_session_ids[0]),
5086                "oldest idle loadable background thread should still be removed"
5087            );
5088            for session_id in &loadable_session_ids[1..6] {
5089                assert!(
5090                    panel.background_threads.contains_key(session_id),
5091                    "more recent idle loadable background threads should be retained"
5092                );
5093            }
5094            assert!(
5095                !panel
5096                    .background_threads
5097                    .contains_key(&loadable_session_ids[6]),
5098                "the active loadable thread should not also be stored as a background thread"
5099            );
5100        });
5101    }
5102
5103    #[gpui::test]
5104    async fn test_thread_target_local_project(cx: &mut TestAppContext) {
5105        init_test(cx);
5106        cx.update(|cx| {
5107            cx.update_flags(true, vec!["agent-v2".to_string()]);
5108            agent::ThreadStore::init_global(cx);
5109            language_model::LanguageModelRegistry::test(cx);
5110        });
5111
5112        let fs = FakeFs::new(cx.executor());
5113        fs.insert_tree(
5114            "/project",
5115            json!({
5116                ".git": {},
5117                "src": {
5118                    "main.rs": "fn main() {}"
5119                }
5120            }),
5121        )
5122        .await;
5123        fs.set_branch_name(Path::new("/project/.git"), Some("main"));
5124
5125        let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
5126
5127        let multi_workspace =
5128            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
5129
5130        let workspace = multi_workspace
5131            .read_with(cx, |multi_workspace, _cx| {
5132                multi_workspace.workspace().clone()
5133            })
5134            .unwrap();
5135
5136        workspace.update(cx, |workspace, _cx| {
5137            workspace.set_random_database_id();
5138        });
5139
5140        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
5141
5142        // Wait for the project to discover the git repository.
5143        cx.run_until_parked();
5144
5145        let panel = workspace.update_in(cx, |workspace, window, cx| {
5146            let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
5147            workspace.add_panel(panel.clone(), window, cx);
5148            panel
5149        });
5150
5151        cx.run_until_parked();
5152
5153        // Default thread target should be LocalProject.
5154        panel.read_with(cx, |panel, _cx| {
5155            assert_eq!(
5156                *panel.start_thread_in(),
5157                StartThreadIn::LocalProject,
5158                "default thread target should be LocalProject"
5159            );
5160        });
5161
5162        // Start a new thread with the default LocalProject target.
5163        // Use StubAgentServer so the thread connects immediately in tests.
5164        panel.update_in(cx, |panel, window, cx| {
5165            panel.open_external_thread_with_server(
5166                Rc::new(StubAgentServer::default_response()),
5167                window,
5168                cx,
5169            );
5170        });
5171
5172        cx.run_until_parked();
5173
5174        // MultiWorkspace should still have exactly one workspace (no worktree created).
5175        multi_workspace
5176            .read_with(cx, |multi_workspace, _cx| {
5177                assert_eq!(
5178                    multi_workspace.workspaces().len(),
5179                    1,
5180                    "LocalProject should not create a new workspace"
5181                );
5182            })
5183            .unwrap();
5184
5185        // The thread should be active in the panel.
5186        panel.read_with(cx, |panel, cx| {
5187            assert!(
5188                panel.active_agent_thread(cx).is_some(),
5189                "a thread should be running in the current workspace"
5190            );
5191        });
5192
5193        // The thread target should still be LocalProject (unchanged).
5194        panel.read_with(cx, |panel, _cx| {
5195            assert_eq!(
5196                *panel.start_thread_in(),
5197                StartThreadIn::LocalProject,
5198                "thread target should remain LocalProject"
5199            );
5200        });
5201
5202        // No worktree creation status should be set.
5203        panel.read_with(cx, |panel, _cx| {
5204            assert!(
5205                panel.worktree_creation_status.is_none(),
5206                "no worktree creation should have occurred"
5207            );
5208        });
5209    }
5210
5211    #[gpui::test]
5212    async fn test_thread_target_serialization_round_trip(cx: &mut TestAppContext) {
5213        init_test(cx);
5214        cx.update(|cx| {
5215            cx.update_flags(true, vec!["agent-v2".to_string()]);
5216            agent::ThreadStore::init_global(cx);
5217            language_model::LanguageModelRegistry::test(cx);
5218        });
5219
5220        let fs = FakeFs::new(cx.executor());
5221        fs.insert_tree(
5222            "/project",
5223            json!({
5224                ".git": {},
5225                "src": {
5226                    "main.rs": "fn main() {}"
5227                }
5228            }),
5229        )
5230        .await;
5231        fs.set_branch_name(Path::new("/project/.git"), Some("main"));
5232
5233        let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
5234
5235        let multi_workspace =
5236            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
5237
5238        let workspace = multi_workspace
5239            .read_with(cx, |multi_workspace, _cx| {
5240                multi_workspace.workspace().clone()
5241            })
5242            .unwrap();
5243
5244        workspace.update(cx, |workspace, _cx| {
5245            workspace.set_random_database_id();
5246        });
5247
5248        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
5249
5250        // Wait for the project to discover the git repository.
5251        cx.run_until_parked();
5252
5253        let panel = workspace.update_in(cx, |workspace, window, cx| {
5254            let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
5255            workspace.add_panel(panel.clone(), window, cx);
5256            panel
5257        });
5258
5259        cx.run_until_parked();
5260
5261        // Default should be LocalProject.
5262        panel.read_with(cx, |panel, _cx| {
5263            assert_eq!(*panel.start_thread_in(), StartThreadIn::LocalProject);
5264        });
5265
5266        // Change thread target to NewWorktree.
5267        panel.update_in(cx, |panel, window, cx| {
5268            panel.set_start_thread_in(&StartThreadIn::NewWorktree, window, cx);
5269        });
5270
5271        panel.read_with(cx, |panel, _cx| {
5272            assert_eq!(
5273                *panel.start_thread_in(),
5274                StartThreadIn::NewWorktree,
5275                "thread target should be NewWorktree after set_thread_target"
5276            );
5277        });
5278
5279        // Let serialization complete.
5280        cx.run_until_parked();
5281
5282        // Load a fresh panel from the serialized data.
5283        let async_cx = cx.update(|window, cx| window.to_async(cx));
5284        let loaded_panel = AgentPanel::load(workspace.downgrade(), async_cx)
5285            .await
5286            .expect("panel load should succeed");
5287        cx.run_until_parked();
5288
5289        loaded_panel.read_with(cx, |panel, _cx| {
5290            assert_eq!(
5291                *panel.start_thread_in(),
5292                StartThreadIn::NewWorktree,
5293                "thread target should survive serialization round-trip"
5294            );
5295        });
5296    }
5297
5298    #[gpui::test]
5299    async fn test_set_active_blocked_during_worktree_creation(cx: &mut TestAppContext) {
5300        init_test(cx);
5301
5302        let fs = FakeFs::new(cx.executor());
5303        cx.update(|cx| {
5304            cx.update_flags(true, vec!["agent-v2".to_string()]);
5305            agent::ThreadStore::init_global(cx);
5306            language_model::LanguageModelRegistry::test(cx);
5307            <dyn fs::Fs>::set_global(fs.clone(), cx);
5308        });
5309
5310        fs.insert_tree(
5311            "/project",
5312            json!({
5313                ".git": {},
5314                "src": {
5315                    "main.rs": "fn main() {}"
5316                }
5317            }),
5318        )
5319        .await;
5320
5321        let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
5322
5323        let multi_workspace =
5324            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
5325
5326        let workspace = multi_workspace
5327            .read_with(cx, |multi_workspace, _cx| {
5328                multi_workspace.workspace().clone()
5329            })
5330            .unwrap();
5331
5332        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
5333
5334        let panel = workspace.update_in(cx, |workspace, window, cx| {
5335            let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
5336            workspace.add_panel(panel.clone(), window, cx);
5337            panel
5338        });
5339
5340        cx.run_until_parked();
5341
5342        // Simulate worktree creation in progress and reset to Uninitialized
5343        panel.update_in(cx, |panel, window, cx| {
5344            panel.worktree_creation_status = Some(WorktreeCreationStatus::Creating);
5345            panel.active_view = ActiveView::Uninitialized;
5346            Panel::set_active(panel, true, window, cx);
5347            assert!(
5348                matches!(panel.active_view, ActiveView::Uninitialized),
5349                "set_active should not create a thread while worktree is being created"
5350            );
5351        });
5352
5353        // Clear the creation status and use open_external_thread_with_server
5354        // (which bypasses new_agent_thread) to verify the panel can transition
5355        // out of Uninitialized. We can't call set_active directly because
5356        // new_agent_thread requires full agent server infrastructure.
5357        panel.update_in(cx, |panel, window, cx| {
5358            panel.worktree_creation_status = None;
5359            panel.active_view = ActiveView::Uninitialized;
5360            panel.open_external_thread_with_server(
5361                Rc::new(StubAgentServer::default_response()),
5362                window,
5363                cx,
5364            );
5365        });
5366
5367        cx.run_until_parked();
5368
5369        panel.read_with(cx, |panel, _cx| {
5370            assert!(
5371                !matches!(panel.active_view, ActiveView::Uninitialized),
5372                "panel should transition out of Uninitialized once worktree creation is cleared"
5373            );
5374        });
5375    }
5376
5377    #[test]
5378    fn test_deserialize_agent_variants() {
5379        // PascalCase (legacy AgentType format, persisted in panel state)
5380        assert_eq!(
5381            serde_json::from_str::<Agent>(r#""NativeAgent""#).unwrap(),
5382            Agent::NativeAgent,
5383        );
5384        assert_eq!(
5385            serde_json::from_str::<Agent>(r#"{"Custom":{"name":"my-agent"}}"#).unwrap(),
5386            Agent::Custom {
5387                id: "my-agent".into(),
5388            },
5389        );
5390
5391        // Legacy TextThread variant deserializes to NativeAgent
5392        assert_eq!(
5393            serde_json::from_str::<Agent>(r#""TextThread""#).unwrap(),
5394            Agent::NativeAgent,
5395        );
5396
5397        // snake_case (canonical format)
5398        assert_eq!(
5399            serde_json::from_str::<Agent>(r#""native_agent""#).unwrap(),
5400            Agent::NativeAgent,
5401        );
5402        assert_eq!(
5403            serde_json::from_str::<Agent>(r#"{"custom":{"name":"my-agent"}}"#).unwrap(),
5404            Agent::Custom {
5405                id: "my-agent".into(),
5406            },
5407        );
5408
5409        // Serialization uses snake_case
5410        assert_eq!(
5411            serde_json::to_string(&Agent::NativeAgent).unwrap(),
5412            r#""native_agent""#,
5413        );
5414        assert_eq!(
5415            serde_json::to_string(&Agent::Custom {
5416                id: "my-agent".into()
5417            })
5418            .unwrap(),
5419            r#"{"custom":{"name":"my-agent"}}"#,
5420        );
5421    }
5422
5423    #[gpui::test]
5424    async fn test_worktree_creation_preserves_selected_agent(cx: &mut TestAppContext) {
5425        init_test(cx);
5426
5427        let app_state = cx.update(|cx| {
5428            cx.update_flags(true, vec!["agent-v2".to_string()]);
5429            agent::ThreadStore::init_global(cx);
5430            language_model::LanguageModelRegistry::test(cx);
5431
5432            let app_state = workspace::AppState::test(cx);
5433            workspace::init(app_state.clone(), cx);
5434            app_state
5435        });
5436
5437        let fs = app_state.fs.as_fake();
5438        fs.insert_tree(
5439            "/project",
5440            json!({
5441                ".git": {},
5442                "src": {
5443                    "main.rs": "fn main() {}"
5444                }
5445            }),
5446        )
5447        .await;
5448        fs.set_branch_name(Path::new("/project/.git"), Some("main"));
5449
5450        let project = Project::test(app_state.fs.clone(), [Path::new("/project")], cx).await;
5451
5452        let multi_workspace =
5453            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
5454
5455        let workspace = multi_workspace
5456            .read_with(cx, |multi_workspace, _cx| {
5457                multi_workspace.workspace().clone()
5458            })
5459            .unwrap();
5460
5461        workspace.update(cx, |workspace, _cx| {
5462            workspace.set_random_database_id();
5463        });
5464
5465        // Register a callback so new workspaces also get an AgentPanel.
5466        cx.update(|cx| {
5467            cx.observe_new(
5468                |workspace: &mut Workspace,
5469                 window: Option<&mut Window>,
5470                 cx: &mut Context<Workspace>| {
5471                    if let Some(window) = window {
5472                        let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
5473                        workspace.add_panel(panel, window, cx);
5474                    }
5475                },
5476            )
5477            .detach();
5478        });
5479
5480        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
5481
5482        // Wait for the project to discover the git repository.
5483        cx.run_until_parked();
5484
5485        let panel = workspace.update_in(cx, |workspace, window, cx| {
5486            let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
5487            workspace.add_panel(panel.clone(), window, cx);
5488            panel
5489        });
5490
5491        cx.run_until_parked();
5492
5493        // Open a thread (needed so there's an active thread view).
5494        panel.update_in(cx, |panel, window, cx| {
5495            panel.open_external_thread_with_server(
5496                Rc::new(StubAgentServer::default_response()),
5497                window,
5498                cx,
5499            );
5500        });
5501
5502        cx.run_until_parked();
5503
5504        // Set the selected agent to Codex (a custom agent) and start_thread_in
5505        // to NewWorktree. We do this AFTER opening the thread because
5506        // open_external_thread_with_server overrides selected_agent.
5507        panel.update_in(cx, |panel, window, cx| {
5508            panel.selected_agent = Agent::Custom {
5509                id: CODEX_ID.into(),
5510            };
5511            panel.set_start_thread_in(&StartThreadIn::NewWorktree, window, cx);
5512        });
5513
5514        // Verify the panel has the Codex agent selected.
5515        panel.read_with(cx, |panel, _cx| {
5516            assert_eq!(
5517                panel.selected_agent,
5518                Agent::Custom {
5519                    id: CODEX_ID.into()
5520                },
5521            );
5522        });
5523
5524        // Directly call handle_worktree_creation_requested, which is what
5525        // handle_first_send_requested does when start_thread_in == NewWorktree.
5526        let content = vec![acp::ContentBlock::Text(acp::TextContent::new(
5527            "Hello from test",
5528        ))];
5529        panel.update_in(cx, |panel, window, cx| {
5530            panel.handle_worktree_creation_requested(content, window, cx);
5531        });
5532
5533        // Let the async worktree creation + workspace setup complete.
5534        cx.run_until_parked();
5535
5536        // Find the new workspace's AgentPanel and verify it used the Codex agent.
5537        let found_codex = multi_workspace
5538            .read_with(cx, |multi_workspace, cx| {
5539                // There should be more than one workspace now (the original + the new worktree).
5540                assert!(
5541                    multi_workspace.workspaces().len() > 1,
5542                    "expected a new workspace to have been created, found {}",
5543                    multi_workspace.workspaces().len(),
5544                );
5545
5546                // Check the newest workspace's panel for the correct agent.
5547                let new_workspace = multi_workspace
5548                    .workspaces()
5549                    .iter()
5550                    .find(|ws| ws.entity_id() != workspace.entity_id())
5551                    .expect("should find the new workspace");
5552                let new_panel = new_workspace
5553                    .read(cx)
5554                    .panel::<AgentPanel>(cx)
5555                    .expect("new workspace should have an AgentPanel");
5556
5557                new_panel.read(cx).selected_agent.clone()
5558            })
5559            .unwrap();
5560
5561        assert_eq!(
5562            found_codex,
5563            Agent::Custom {
5564                id: CODEX_ID.into()
5565            },
5566            "the new worktree workspace should use the same agent (Codex) that was selected in the original panel",
5567        );
5568    }
5569
5570    #[gpui::test]
5571    async fn test_work_dirs_update_when_worktrees_change(cx: &mut TestAppContext) {
5572        use crate::thread_metadata_store::ThreadMetadataStore;
5573
5574        init_test(cx);
5575        cx.update(|cx| {
5576            cx.update_flags(true, vec!["agent-v2".to_string()]);
5577            agent::ThreadStore::init_global(cx);
5578            language_model::LanguageModelRegistry::test(cx);
5579        });
5580
5581        // Set up a project with one worktree.
5582        let fs = FakeFs::new(cx.executor());
5583        fs.insert_tree("/project_a", json!({ "file.txt": "" }))
5584            .await;
5585        let project = Project::test(fs.clone(), [Path::new("/project_a")], cx).await;
5586
5587        let multi_workspace =
5588            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
5589        let workspace = multi_workspace
5590            .read_with(cx, |mw, _cx| mw.workspace().clone())
5591            .unwrap();
5592        let mut cx = VisualTestContext::from_window(multi_workspace.into(), cx);
5593
5594        let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
5595            cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
5596        });
5597
5598        // Open thread A and send a message. With empty next_prompt_updates it
5599        // stays generating, so opening B will move A to background_threads.
5600        let connection_a = StubAgentConnection::new().with_agent_id("agent-a".into());
5601        open_thread_with_custom_connection(&panel, connection_a.clone(), &mut cx);
5602        send_message(&panel, &mut cx);
5603        let session_id_a = active_session_id(&panel, &cx);
5604
5605        // Open thread C — thread A (generating) moves to background.
5606        // Thread C completes immediately (idle), then opening B moves C to background too.
5607        let connection_c = StubAgentConnection::new().with_agent_id("agent-c".into());
5608        connection_c.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
5609            acp::ContentChunk::new("done".into()),
5610        )]);
5611        open_thread_with_custom_connection(&panel, connection_c.clone(), &mut cx);
5612        send_message(&panel, &mut cx);
5613        let session_id_c = active_session_id(&panel, &cx);
5614
5615        // Open thread B — thread C (idle, non-loadable) is retained in background.
5616        let connection_b = StubAgentConnection::new().with_agent_id("agent-b".into());
5617        open_thread_with_custom_connection(&panel, connection_b.clone(), &mut cx);
5618        send_message(&panel, &mut cx);
5619        let session_id_b = active_session_id(&panel, &cx);
5620
5621        let metadata_store = cx.update(|_, cx| ThreadMetadataStore::global(cx));
5622
5623        panel.read_with(&cx, |panel, _cx| {
5624            assert!(
5625                panel.background_threads.contains_key(&session_id_a),
5626                "Thread A should be in background_threads"
5627            );
5628            assert!(
5629                panel.background_threads.contains_key(&session_id_c),
5630                "Thread C should be in background_threads"
5631            );
5632        });
5633
5634        // Verify initial work_dirs for thread B contain only /project_a.
5635        let initial_b_paths = panel.read_with(&cx, |panel, cx| {
5636            let thread = panel.active_agent_thread(cx).unwrap();
5637            thread.read(cx).work_dirs().cloned().unwrap()
5638        });
5639        assert_eq!(
5640            initial_b_paths.ordered_paths().collect::<Vec<_>>(),
5641            vec![&PathBuf::from("/project_a")],
5642            "Thread B should initially have only /project_a"
5643        );
5644
5645        // Now add a second worktree to the project.
5646        fs.insert_tree("/project_b", json!({ "other.txt": "" }))
5647            .await;
5648        let (new_tree, _) = project
5649            .update(&mut cx, |project, cx| {
5650                project.find_or_create_worktree("/project_b", true, cx)
5651            })
5652            .await
5653            .unwrap();
5654        cx.read(|cx| new_tree.read(cx).as_local().unwrap().scan_complete())
5655            .await;
5656        cx.run_until_parked();
5657
5658        // Verify thread B's (active) work_dirs now include both worktrees.
5659        let updated_b_paths = panel.read_with(&cx, |panel, cx| {
5660            let thread = panel.active_agent_thread(cx).unwrap();
5661            thread.read(cx).work_dirs().cloned().unwrap()
5662        });
5663        let mut b_paths_sorted = updated_b_paths.ordered_paths().cloned().collect::<Vec<_>>();
5664        b_paths_sorted.sort();
5665        assert_eq!(
5666            b_paths_sorted,
5667            vec![PathBuf::from("/project_a"), PathBuf::from("/project_b")],
5668            "Thread B work_dirs should include both worktrees after adding /project_b"
5669        );
5670
5671        // Verify thread A's (background) work_dirs are also updated.
5672        let updated_a_paths = panel.read_with(&cx, |panel, cx| {
5673            let bg_view = panel.background_threads.get(&session_id_a).unwrap();
5674            let root_thread = bg_view.read(cx).root_thread(cx).unwrap();
5675            root_thread
5676                .read(cx)
5677                .thread
5678                .read(cx)
5679                .work_dirs()
5680                .cloned()
5681                .unwrap()
5682        });
5683        let mut a_paths_sorted = updated_a_paths.ordered_paths().cloned().collect::<Vec<_>>();
5684        a_paths_sorted.sort();
5685        assert_eq!(
5686            a_paths_sorted,
5687            vec![PathBuf::from("/project_a"), PathBuf::from("/project_b")],
5688            "Thread A work_dirs should include both worktrees after adding /project_b"
5689        );
5690
5691        // Verify thread idle C was also updated.
5692        let updated_c_paths = panel.read_with(&cx, |panel, cx| {
5693            let bg_view = panel.background_threads.get(&session_id_c).unwrap();
5694            let root_thread = bg_view.read(cx).root_thread(cx).unwrap();
5695            root_thread
5696                .read(cx)
5697                .thread
5698                .read(cx)
5699                .work_dirs()
5700                .cloned()
5701                .unwrap()
5702        });
5703        let mut c_paths_sorted = updated_c_paths.ordered_paths().cloned().collect::<Vec<_>>();
5704        c_paths_sorted.sort();
5705        assert_eq!(
5706            c_paths_sorted,
5707            vec![PathBuf::from("/project_a"), PathBuf::from("/project_b")],
5708            "Thread C (idle background) work_dirs should include both worktrees after adding /project_b"
5709        );
5710
5711        // Verify the metadata store reflects the new paths for running threads only.
5712        cx.run_until_parked();
5713        for (label, session_id) in [("thread B", &session_id_b), ("thread A", &session_id_a)] {
5714            let metadata_paths = metadata_store.read_with(&cx, |store, _cx| {
5715                let metadata = store
5716                    .entry(session_id)
5717                    .unwrap_or_else(|| panic!("{label} thread metadata should exist"));
5718                metadata.folder_paths.clone()
5719            });
5720            let mut sorted = metadata_paths.ordered_paths().cloned().collect::<Vec<_>>();
5721            sorted.sort();
5722            assert_eq!(
5723                sorted,
5724                vec![PathBuf::from("/project_a"), PathBuf::from("/project_b")],
5725                "{label} thread metadata folder_paths should include both worktrees"
5726            );
5727        }
5728
5729        // Now remove a worktree and verify work_dirs shrink.
5730        let worktree_b_id = new_tree.read_with(&cx, |tree, _| tree.id());
5731        project.update(&mut cx, |project, cx| {
5732            project.remove_worktree(worktree_b_id, cx);
5733        });
5734        cx.run_until_parked();
5735
5736        let after_remove_b = panel.read_with(&cx, |panel, cx| {
5737            let thread = panel.active_agent_thread(cx).unwrap();
5738            thread.read(cx).work_dirs().cloned().unwrap()
5739        });
5740        assert_eq!(
5741            after_remove_b.ordered_paths().collect::<Vec<_>>(),
5742            vec![&PathBuf::from("/project_a")],
5743            "Thread B work_dirs should revert to only /project_a after removing /project_b"
5744        );
5745
5746        let after_remove_a = panel.read_with(&cx, |panel, cx| {
5747            let bg_view = panel.background_threads.get(&session_id_a).unwrap();
5748            let root_thread = bg_view.read(cx).root_thread(cx).unwrap();
5749            root_thread
5750                .read(cx)
5751                .thread
5752                .read(cx)
5753                .work_dirs()
5754                .cloned()
5755                .unwrap()
5756        });
5757        assert_eq!(
5758            after_remove_a.ordered_paths().collect::<Vec<_>>(),
5759            vec![&PathBuf::from("/project_a")],
5760            "Thread A work_dirs should revert to only /project_a after removing /project_b"
5761        );
5762    }
5763
5764    #[gpui::test]
5765    async fn test_new_workspace_inherits_global_last_used_agent(cx: &mut TestAppContext) {
5766        init_test(cx);
5767        cx.update(|cx| {
5768            cx.update_flags(true, vec!["agent-v2".to_string()]);
5769            agent::ThreadStore::init_global(cx);
5770            language_model::LanguageModelRegistry::test(cx);
5771            // Use an isolated DB so parallel tests can't overwrite our global key.
5772            cx.set_global(db::AppDatabase::test_new());
5773        });
5774
5775        let custom_agent = Agent::Custom {
5776            id: "my-preferred-agent".into(),
5777        };
5778
5779        // Write a known agent to the global KVP to simulate a user who has
5780        // previously used this agent in another workspace.
5781        let kvp = cx.update(|cx| KeyValueStore::global(cx));
5782        write_global_last_used_agent(kvp, custom_agent.clone()).await;
5783
5784        let fs = FakeFs::new(cx.executor());
5785        let project = Project::test(fs.clone(), [], cx).await;
5786
5787        let multi_workspace =
5788            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
5789
5790        let workspace = multi_workspace
5791            .read_with(cx, |multi_workspace, _cx| {
5792                multi_workspace.workspace().clone()
5793            })
5794            .unwrap();
5795
5796        workspace.update(cx, |workspace, _cx| {
5797            workspace.set_random_database_id();
5798        });
5799
5800        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
5801
5802        // Load the panel via `load()`, which reads the global fallback
5803        // asynchronously when no per-workspace state exists.
5804        let async_cx = cx.update(|window, cx| window.to_async(cx));
5805        let panel = AgentPanel::load(workspace.downgrade(), async_cx)
5806            .await
5807            .expect("panel load should succeed");
5808        cx.run_until_parked();
5809
5810        panel.read_with(cx, |panel, _cx| {
5811            assert_eq!(
5812                panel.selected_agent, custom_agent,
5813                "new workspace should inherit the global last-used agent"
5814            );
5815        });
5816    }
5817
5818    #[gpui::test]
5819    async fn test_workspaces_maintain_independent_agent_selection(cx: &mut TestAppContext) {
5820        init_test(cx);
5821        cx.update(|cx| {
5822            cx.update_flags(true, vec!["agent-v2".to_string()]);
5823            agent::ThreadStore::init_global(cx);
5824            language_model::LanguageModelRegistry::test(cx);
5825        });
5826
5827        let fs = FakeFs::new(cx.executor());
5828        let project_a = Project::test(fs.clone(), [], cx).await;
5829        let project_b = Project::test(fs, [], cx).await;
5830
5831        let multi_workspace =
5832            cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
5833
5834        let workspace_a = multi_workspace
5835            .read_with(cx, |multi_workspace, _cx| {
5836                multi_workspace.workspace().clone()
5837            })
5838            .unwrap();
5839
5840        let workspace_b = multi_workspace
5841            .update(cx, |multi_workspace, window, cx| {
5842                multi_workspace.test_add_workspace(project_b.clone(), window, cx)
5843            })
5844            .unwrap();
5845
5846        workspace_a.update(cx, |workspace, _cx| {
5847            workspace.set_random_database_id();
5848        });
5849        workspace_b.update(cx, |workspace, _cx| {
5850            workspace.set_random_database_id();
5851        });
5852
5853        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
5854
5855        let agent_a = Agent::Custom {
5856            id: "agent-alpha".into(),
5857        };
5858        let agent_b = Agent::Custom {
5859            id: "agent-beta".into(),
5860        };
5861
5862        // Set up workspace A with agent_a
5863        let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
5864            cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
5865        });
5866        panel_a.update(cx, |panel, _cx| {
5867            panel.selected_agent = agent_a.clone();
5868        });
5869
5870        // Set up workspace B with agent_b
5871        let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
5872            cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
5873        });
5874        panel_b.update(cx, |panel, _cx| {
5875            panel.selected_agent = agent_b.clone();
5876        });
5877
5878        // Serialize both panels
5879        panel_a.update(cx, |panel, cx| panel.serialize(cx));
5880        panel_b.update(cx, |panel, cx| panel.serialize(cx));
5881        cx.run_until_parked();
5882
5883        // Load fresh panels from serialized state and verify independence
5884        let async_cx = cx.update(|window, cx| window.to_async(cx));
5885        let loaded_a = AgentPanel::load(workspace_a.downgrade(), async_cx)
5886            .await
5887            .expect("panel A load should succeed");
5888        cx.run_until_parked();
5889
5890        let async_cx = cx.update(|window, cx| window.to_async(cx));
5891        let loaded_b = AgentPanel::load(workspace_b.downgrade(), async_cx)
5892            .await
5893            .expect("panel B load should succeed");
5894        cx.run_until_parked();
5895
5896        loaded_a.read_with(cx, |panel, _cx| {
5897            assert_eq!(
5898                panel.selected_agent, agent_a,
5899                "workspace A should restore agent-alpha, not agent-beta"
5900            );
5901        });
5902
5903        loaded_b.read_with(cx, |panel, _cx| {
5904            assert_eq!(
5905                panel.selected_agent, agent_b,
5906                "workspace B should restore agent-beta, not agent-alpha"
5907            );
5908        });
5909    }
5910
5911    #[gpui::test]
5912    async fn test_new_thread_uses_workspace_selected_agent(cx: &mut TestAppContext) {
5913        init_test(cx);
5914        cx.update(|cx| {
5915            cx.update_flags(true, vec!["agent-v2".to_string()]);
5916            agent::ThreadStore::init_global(cx);
5917            language_model::LanguageModelRegistry::test(cx);
5918        });
5919
5920        let fs = FakeFs::new(cx.executor());
5921        let project = Project::test(fs.clone(), [], cx).await;
5922
5923        let multi_workspace =
5924            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
5925
5926        let workspace = multi_workspace
5927            .read_with(cx, |multi_workspace, _cx| {
5928                multi_workspace.workspace().clone()
5929            })
5930            .unwrap();
5931
5932        workspace.update(cx, |workspace, _cx| {
5933            workspace.set_random_database_id();
5934        });
5935
5936        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
5937
5938        let custom_agent = Agent::Custom {
5939            id: "my-custom-agent".into(),
5940        };
5941
5942        let panel = workspace.update_in(cx, |workspace, window, cx| {
5943            let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
5944            workspace.add_panel(panel.clone(), window, cx);
5945            panel
5946        });
5947
5948        // Set selected_agent to a custom agent
5949        panel.update(cx, |panel, _cx| {
5950            panel.selected_agent = custom_agent.clone();
5951        });
5952
5953        // Call new_thread, which internally calls external_thread(None, ...)
5954        // This resolves the agent from self.selected_agent
5955        panel.update_in(cx, |panel, window, cx| {
5956            panel.new_thread(&NewThread, window, cx);
5957        });
5958
5959        panel.read_with(cx, |panel, _cx| {
5960            assert_eq!(
5961                panel.selected_agent, custom_agent,
5962                "selected_agent should remain the custom agent after new_thread"
5963            );
5964            assert!(
5965                panel.active_conversation_view().is_some(),
5966                "a thread should have been created"
5967            );
5968        });
5969    }
5970}