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