agent_panel.rs

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