agent_panel.rs

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