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