agent_panel.rs

   1use std::{
   2    ops::Range,
   3    path::{Path, PathBuf},
   4    rc::Rc,
   5    sync::{
   6        Arc,
   7        atomic::{AtomicBool, Ordering},
   8    },
   9    time::Duration,
  10};
  11
  12use acp_thread::{AcpThread, MentionUri, ThreadStatus};
  13use agent::{ContextServerRegistry, SharedThread, ThreadStore};
  14use agent_client_protocol as acp;
  15use agent_servers::AgentServer;
  16use db::kvp::{Dismissable, KEY_VALUE_STORE};
  17use itertools::Itertools;
  18use project::{
  19    ExternalAgentServerName,
  20    agent_server_store::{CLAUDE_AGENT_NAME, CODEX_NAME, GEMINI_NAME},
  21};
  22use serde::{Deserialize, Serialize};
  23use settings::{LanguageModelProviderSetting, LanguageModelSelection};
  24
  25use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt as _};
  26use zed_actions::agent::{OpenClaudeAgentOnboardingModal, ReauthenticateAgent, ReviewBranchDiff};
  27
  28use crate::ManageProfiles;
  29use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal};
  30use crate::{
  31    AddContextServer, AgentDiffPane, ConnectionView, CopyThreadToClipboard, Follow,
  32    InlineAssistant, LoadThreadFromClipboard, NewTextThread, NewThread, OpenActiveThreadAsMarkdown,
  33    OpenAgentDiff, OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell, StartThreadIn,
  34    ToggleNavigationMenu, ToggleNewThreadMenu, ToggleOptionsMenu, ToggleStartThreadInSelector,
  35    agent_configuration::{AgentConfiguration, AssistantConfigurationEvent},
  36    connection_view::{AcpThreadViewEvent, ThreadView},
  37    slash_command::SlashCommandCompletionProvider,
  38    text_thread_editor::{AgentPanelDelegate, TextThreadEditor, make_lsp_adapter_delegate},
  39    ui::EndTrialUpsell,
  40};
  41use crate::{
  42    AgentInitialContent, ExternalAgent, ExternalSourcePrompt, NewExternalAgentThread,
  43    NewNativeAgentThreadFromSummary,
  44};
  45use crate::{
  46    ExpandMessageEditor, ThreadHistory, ThreadHistoryEvent,
  47    text_thread_history::{TextThreadHistory, TextThreadHistoryEvent},
  48};
  49use agent_settings::AgentSettings;
  50use ai_onboarding::AgentPanelOnboarding;
  51use anyhow::{Result, anyhow};
  52use assistant_slash_command::SlashCommandWorkingSet;
  53use assistant_text_thread::{TextThread, TextThreadEvent, TextThreadSummary};
  54use client::UserStore;
  55use cloud_api_types::Plan;
  56use collections::HashMap;
  57use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
  58use extension::ExtensionEvents;
  59use extension_host::ExtensionStore;
  60use fs::Fs;
  61use git::repository::validate_worktree_directory;
  62use gpui::{
  63    Action, Animation, AnimationExt, AnyElement, App, AsyncWindowContext, ClipboardItem, Corner,
  64    DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, KeyContext, Pixels,
  65    Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
  66};
  67use language::LanguageRegistry;
  68use language_model::{ConfigurationError, LanguageModelRegistry};
  69use project::project_settings::ProjectSettings;
  70use project::{Project, ProjectPath, Worktree};
  71use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
  72use rules_library::{RulesLibrary, open_rules_library};
  73use search::{BufferSearchBar, buffer_search};
  74use settings::{Settings, update_settings_file};
  75use theme::ThemeSettings;
  76use ui::{
  77    Button, ButtonLike, Callout, ContextMenu, ContextMenuEntry, DocumentationSide, KeyBinding,
  78    PopoverMenu, PopoverMenuHandle, SpinnerLabel, Tab, TintColor, Tooltip, prelude::*,
  79    utils::WithRemSize,
  80};
  81use util::ResultExt as _;
  82use workspace::{
  83    CollaboratorId, DraggedSelection, DraggedTab, ToggleZoom, ToolbarItemView, Workspace,
  84    WorkspaceId,
  85    dock::{DockPosition, Panel, PanelEvent},
  86};
  87use zed_actions::{
  88    DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize,
  89    agent::{OpenAcpOnboardingModal, OpenSettings, ResetAgentZoom, ResetOnboarding},
  90    assistant::{OpenRulesLibrary, Toggle, ToggleFocus},
  91};
  92
  93const AGENT_PANEL_KEY: &str = "agent_panel";
  94const RECENTLY_UPDATED_MENU_LIMIT: usize = 6;
  95const DEFAULT_THREAD_TITLE: &str = "New Thread";
  96
  97fn read_serialized_panel(workspace_id: workspace::WorkspaceId) -> Option<SerializedAgentPanel> {
  98    let scope = KEY_VALUE_STORE.scoped(AGENT_PANEL_KEY);
  99    let key = i64::from(workspace_id).to_string();
 100    scope
 101        .read(&key)
 102        .log_err()
 103        .flatten()
 104        .and_then(|json| serde_json::from_str::<SerializedAgentPanel>(&json).log_err())
 105}
 106
 107async fn save_serialized_panel(
 108    workspace_id: workspace::WorkspaceId,
 109    panel: SerializedAgentPanel,
 110) -> Result<()> {
 111    let scope = KEY_VALUE_STORE.scoped(AGENT_PANEL_KEY);
 112    let key = i64::from(workspace_id).to_string();
 113    scope.write(key, serde_json::to_string(&panel)?).await?;
 114    Ok(())
 115}
 116
 117/// Migration: reads the original single-panel format stored under the
 118/// `"agent_panel"` KVP key before per-workspace keying was introduced.
 119fn read_legacy_serialized_panel() -> Option<SerializedAgentPanel> {
 120    KEY_VALUE_STORE
 121        .read_kvp(AGENT_PANEL_KEY)
 122        .log_err()
 123        .flatten()
 124        .and_then(|json| serde_json::from_str::<SerializedAgentPanel>(&json).log_err())
 125}
 126
 127#[derive(Serialize, Deserialize, Debug, Clone)]
 128struct SerializedAgentPanel {
 129    width: Option<Pixels>,
 130    selected_agent: Option<AgentType>,
 131    #[serde(default)]
 132    last_active_thread: Option<SerializedActiveThread>,
 133    #[serde(default)]
 134    start_thread_in: Option<StartThreadIn>,
 135}
 136
 137#[derive(Serialize, Deserialize, Debug, Clone)]
 138struct SerializedActiveThread {
 139    session_id: String,
 140    agent_type: AgentType,
 141    title: Option<String>,
 142    cwd: Option<std::path::PathBuf>,
 143}
 144
 145pub fn init(cx: &mut App) {
 146    cx.observe_new(
 147        |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
 148            workspace
 149                .register_action(|workspace, action: &NewThread, window, cx| {
 150                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 151                        panel.update(cx, |panel, cx| panel.new_thread(action, window, cx));
 152                        workspace.focus_panel::<AgentPanel>(window, cx);
 153                    }
 154                })
 155                .register_action(
 156                    |workspace, action: &NewNativeAgentThreadFromSummary, window, cx| {
 157                        if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 158                            panel.update(cx, |panel, cx| {
 159                                panel.new_native_agent_thread_from_summary(action, window, cx)
 160                            });
 161                            workspace.focus_panel::<AgentPanel>(window, cx);
 162                        }
 163                    },
 164                )
 165                .register_action(|workspace, _: &ExpandMessageEditor, window, cx| {
 166                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 167                        workspace.focus_panel::<AgentPanel>(window, cx);
 168                        panel.update(cx, |panel, cx| panel.expand_message_editor(window, cx));
 169                    }
 170                })
 171                .register_action(|workspace, _: &OpenHistory, window, cx| {
 172                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 173                        workspace.focus_panel::<AgentPanel>(window, cx);
 174                        panel.update(cx, |panel, cx| panel.open_history(window, cx));
 175                    }
 176                })
 177                .register_action(|workspace, _: &OpenSettings, window, cx| {
 178                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 179                        workspace.focus_panel::<AgentPanel>(window, cx);
 180                        panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
 181                    }
 182                })
 183                .register_action(|workspace, _: &NewTextThread, window, cx| {
 184                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 185                        workspace.focus_panel::<AgentPanel>(window, cx);
 186                        panel.update(cx, |panel, cx| {
 187                            panel.new_text_thread(window, cx);
 188                        });
 189                    }
 190                })
 191                .register_action(|workspace, action: &NewExternalAgentThread, window, cx| {
 192                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 193                        workspace.focus_panel::<AgentPanel>(window, cx);
 194                        panel.update(cx, |panel, cx| {
 195                            panel.external_thread(
 196                                action.agent.clone(),
 197                                None,
 198                                None,
 199                                None,
 200                                None,
 201                                true,
 202                                window,
 203                                cx,
 204                            )
 205                        });
 206                    }
 207                })
 208                .register_action(|workspace, action: &OpenRulesLibrary, window, cx| {
 209                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 210                        workspace.focus_panel::<AgentPanel>(window, cx);
 211                        panel.update(cx, |panel, cx| {
 212                            panel.deploy_rules_library(action, window, cx)
 213                        });
 214                    }
 215                })
 216                .register_action(|workspace, _: &Follow, window, cx| {
 217                    workspace.follow(CollaboratorId::Agent, window, cx);
 218                })
 219                .register_action(|workspace, _: &OpenAgentDiff, window, cx| {
 220                    let thread = workspace
 221                        .panel::<AgentPanel>(cx)
 222                        .and_then(|panel| panel.read(cx).active_connection_view().cloned())
 223                        .and_then(|thread_view| {
 224                            thread_view
 225                                .read(cx)
 226                                .active_thread()
 227                                .map(|r| r.read(cx).thread.clone())
 228                        });
 229
 230                    if let Some(thread) = thread {
 231                        AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
 232                    }
 233                })
 234                .register_action(|workspace, _: &ToggleNavigationMenu, window, cx| {
 235                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 236                        workspace.focus_panel::<AgentPanel>(window, cx);
 237                        panel.update(cx, |panel, cx| {
 238                            panel.toggle_navigation_menu(&ToggleNavigationMenu, window, cx);
 239                        });
 240                    }
 241                })
 242                .register_action(|workspace, _: &ToggleOptionsMenu, window, cx| {
 243                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 244                        workspace.focus_panel::<AgentPanel>(window, cx);
 245                        panel.update(cx, |panel, cx| {
 246                            panel.toggle_options_menu(&ToggleOptionsMenu, window, cx);
 247                        });
 248                    }
 249                })
 250                .register_action(|workspace, _: &ToggleNewThreadMenu, window, cx| {
 251                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 252                        workspace.focus_panel::<AgentPanel>(window, cx);
 253                        panel.update(cx, |panel, cx| {
 254                            panel.toggle_new_thread_menu(&ToggleNewThreadMenu, window, cx);
 255                        });
 256                    }
 257                })
 258                .register_action(|workspace, _: &ToggleStartThreadInSelector, window, cx| {
 259                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 260                        workspace.focus_panel::<AgentPanel>(window, cx);
 261                        panel.update(cx, |panel, cx| {
 262                            panel.toggle_start_thread_in_selector(
 263                                &ToggleStartThreadInSelector,
 264                                window,
 265                                cx,
 266                            );
 267                        });
 268                    }
 269                })
 270                .register_action(|workspace, _: &OpenAcpOnboardingModal, window, cx| {
 271                    AcpOnboardingModal::toggle(workspace, window, cx)
 272                })
 273                .register_action(
 274                    |workspace, _: &OpenClaudeAgentOnboardingModal, window, cx| {
 275                        ClaudeCodeOnboardingModal::toggle(workspace, window, cx)
 276                    },
 277                )
 278                .register_action(|_workspace, _: &ResetOnboarding, window, cx| {
 279                    window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
 280                    window.refresh();
 281                })
 282                .register_action(|workspace, _: &ResetTrialUpsell, _window, cx| {
 283                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 284                        panel.update(cx, |panel, _| {
 285                            panel
 286                                .on_boarding_upsell_dismissed
 287                                .store(false, Ordering::Release);
 288                        });
 289                    }
 290                    OnboardingUpsell::set_dismissed(false, cx);
 291                })
 292                .register_action(|_workspace, _: &ResetTrialEndUpsell, _window, cx| {
 293                    TrialEndUpsell::set_dismissed(false, cx);
 294                })
 295                .register_action(|workspace, _: &ResetAgentZoom, window, cx| {
 296                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 297                        panel.update(cx, |panel, cx| {
 298                            panel.reset_agent_zoom(window, cx);
 299                        });
 300                    }
 301                })
 302                .register_action(|workspace, _: &CopyThreadToClipboard, window, cx| {
 303                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 304                        panel.update(cx, |panel, cx| {
 305                            panel.copy_thread_to_clipboard(window, cx);
 306                        });
 307                    }
 308                })
 309                .register_action(|workspace, _: &LoadThreadFromClipboard, window, cx| {
 310                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 311                        workspace.focus_panel::<AgentPanel>(window, cx);
 312                        panel.update(cx, |panel, cx| {
 313                            panel.load_thread_from_clipboard(window, cx);
 314                        });
 315                    }
 316                })
 317                .register_action(|workspace, action: &ReviewBranchDiff, window, cx| {
 318                    let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
 319                        return;
 320                    };
 321
 322                    let mention_uri = MentionUri::GitDiff {
 323                        base_ref: action.base_ref.to_string(),
 324                    };
 325                    let diff_uri = mention_uri.to_uri().to_string();
 326
 327                    let content_blocks = vec![
 328                        acp::ContentBlock::Text(acp::TextContent::new(
 329                            "Please review this branch diff carefully. Point out any issues, \
 330                             potential bugs, or improvement opportunities you find.\n\n"
 331                                .to_string(),
 332                        )),
 333                        acp::ContentBlock::Resource(acp::EmbeddedResource::new(
 334                            acp::EmbeddedResourceResource::TextResourceContents(
 335                                acp::TextResourceContents::new(
 336                                    action.diff_text.to_string(),
 337                                    diff_uri,
 338                                ),
 339                            ),
 340                        )),
 341                    ];
 342
 343                    workspace.focus_panel::<AgentPanel>(window, cx);
 344
 345                    panel.update(cx, |panel, cx| {
 346                        panel.external_thread(
 347                            None,
 348                            None,
 349                            None,
 350                            None,
 351                            Some(AgentInitialContent::ContentBlock {
 352                                blocks: content_blocks,
 353                                auto_submit: true,
 354                            }),
 355                            true,
 356                            window,
 357                            cx,
 358                        );
 359                    });
 360                })
 361                .register_action(|workspace, action: &StartThreadIn, _window, cx| {
 362                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 363                        panel.update(cx, |panel, cx| {
 364                            panel.set_start_thread_in(action, cx);
 365                        });
 366                    }
 367                });
 368        },
 369    )
 370    .detach();
 371}
 372
 373#[derive(Clone, Copy, Debug, PartialEq, Eq)]
 374enum HistoryKind {
 375    AgentThreads,
 376    TextThreads,
 377}
 378
 379enum ActiveView {
 380    Uninitialized,
 381    AgentThread {
 382        server_view: Entity<ConnectionView>,
 383    },
 384    TextThread {
 385        text_thread_editor: Entity<TextThreadEditor>,
 386        title_editor: Entity<Editor>,
 387        buffer_search_bar: Entity<BufferSearchBar>,
 388        _subscriptions: Vec<gpui::Subscription>,
 389    },
 390    History {
 391        kind: HistoryKind,
 392    },
 393    Configuration,
 394}
 395
 396enum WhichFontSize {
 397    AgentFont,
 398    BufferFont,
 399    None,
 400}
 401
 402// TODO unify this with ExternalAgent
 403#[derive(Debug, Default, Clone, PartialEq, Serialize)]
 404pub enum AgentType {
 405    #[default]
 406    NativeAgent,
 407    TextThread,
 408    Custom {
 409        name: SharedString,
 410    },
 411}
 412
 413// Custom impl handles legacy variant names from before the built-in agents were moved to
 414// the registry: "ClaudeAgent" -> Custom { name: "claude-acp" }, "Codex" -> Custom { name:
 415// "codex-acp" }, "Gemini" -> Custom { name: "gemini" }.
 416// Can be removed at some point in the future and go back to #[derive(Deserialize)].
 417impl<'de> Deserialize<'de> for AgentType {
 418    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
 419    where
 420        D: serde::Deserializer<'de>,
 421    {
 422        let value = serde_json::Value::deserialize(deserializer)?;
 423
 424        if let Some(s) = value.as_str() {
 425            return match s {
 426                "NativeAgent" => Ok(Self::NativeAgent),
 427                "TextThread" => Ok(Self::TextThread),
 428                "ClaudeAgent" | "ClaudeCode" => Ok(Self::Custom {
 429                    name: CLAUDE_AGENT_NAME.into(),
 430                }),
 431                "Codex" => Ok(Self::Custom {
 432                    name: CODEX_NAME.into(),
 433                }),
 434                "Gemini" => Ok(Self::Custom {
 435                    name: GEMINI_NAME.into(),
 436                }),
 437                other => Err(serde::de::Error::unknown_variant(
 438                    other,
 439                    &[
 440                        "NativeAgent",
 441                        "TextThread",
 442                        "Custom",
 443                        "ClaudeAgent",
 444                        "ClaudeCode",
 445                        "Codex",
 446                        "Gemini",
 447                    ],
 448                )),
 449            };
 450        }
 451
 452        if let Some(obj) = value.as_object() {
 453            if let Some(inner) = obj.get("Custom") {
 454                #[derive(Deserialize)]
 455                struct CustomFields {
 456                    name: SharedString,
 457                }
 458                let fields: CustomFields =
 459                    serde_json::from_value(inner.clone()).map_err(serde::de::Error::custom)?;
 460                return Ok(Self::Custom { name: fields.name });
 461            }
 462        }
 463
 464        Err(serde::de::Error::custom(
 465            "expected a string variant or {\"Custom\": {\"name\": ...}}",
 466        ))
 467    }
 468}
 469
 470impl AgentType {
 471    pub fn is_native(&self) -> bool {
 472        matches!(self, Self::NativeAgent)
 473    }
 474
 475    fn label(&self) -> SharedString {
 476        match self {
 477            Self::NativeAgent | Self::TextThread => "Zed Agent".into(),
 478            Self::Custom { name, .. } => name.into(),
 479        }
 480    }
 481
 482    fn icon(&self) -> Option<IconName> {
 483        match self {
 484            Self::NativeAgent | Self::TextThread => None,
 485            Self::Custom { .. } => Some(IconName::Sparkle),
 486        }
 487    }
 488}
 489
 490impl From<ExternalAgent> for AgentType {
 491    fn from(value: ExternalAgent) -> Self {
 492        match value {
 493            ExternalAgent::Custom { name } => Self::Custom { name },
 494            ExternalAgent::NativeAgent => Self::NativeAgent,
 495        }
 496    }
 497}
 498
 499impl StartThreadIn {
 500    fn label(&self) -> SharedString {
 501        match self {
 502            Self::LocalProject => "Current Project".into(),
 503            Self::NewWorktree => "New Worktree".into(),
 504        }
 505    }
 506}
 507
 508#[derive(Clone, Debug)]
 509#[allow(dead_code)]
 510pub enum WorktreeCreationStatus {
 511    Creating,
 512    Error(SharedString),
 513}
 514
 515impl ActiveView {
 516    pub fn which_font_size_used(&self) -> WhichFontSize {
 517        match self {
 518            ActiveView::Uninitialized
 519            | ActiveView::AgentThread { .. }
 520            | ActiveView::History { .. } => WhichFontSize::AgentFont,
 521            ActiveView::TextThread { .. } => WhichFontSize::BufferFont,
 522            ActiveView::Configuration => WhichFontSize::None,
 523        }
 524    }
 525
 526    pub fn text_thread(
 527        text_thread_editor: Entity<TextThreadEditor>,
 528        language_registry: Arc<LanguageRegistry>,
 529        window: &mut Window,
 530        cx: &mut App,
 531    ) -> Self {
 532        let title = text_thread_editor.read(cx).title(cx).to_string();
 533
 534        let editor = cx.new(|cx| {
 535            let mut editor = Editor::single_line(window, cx);
 536            editor.set_text(title, window, cx);
 537            editor
 538        });
 539
 540        // This is a workaround for `editor.set_text` emitting a `BufferEdited` event, which would
 541        // cause a custom summary to be set. The presence of this custom summary would cause
 542        // summarization to not happen.
 543        let mut suppress_first_edit = true;
 544
 545        let subscriptions = vec![
 546            window.subscribe(&editor, cx, {
 547                {
 548                    let text_thread_editor = text_thread_editor.clone();
 549                    move |editor, event, window, cx| match event {
 550                        EditorEvent::BufferEdited => {
 551                            if suppress_first_edit {
 552                                suppress_first_edit = false;
 553                                return;
 554                            }
 555                            let new_summary = editor.read(cx).text(cx);
 556
 557                            text_thread_editor.update(cx, |text_thread_editor, cx| {
 558                                text_thread_editor
 559                                    .text_thread()
 560                                    .update(cx, |text_thread, cx| {
 561                                        text_thread.set_custom_summary(new_summary, cx);
 562                                    })
 563                            })
 564                        }
 565                        EditorEvent::Blurred => {
 566                            if editor.read(cx).text(cx).is_empty() {
 567                                let summary = text_thread_editor
 568                                    .read(cx)
 569                                    .text_thread()
 570                                    .read(cx)
 571                                    .summary()
 572                                    .or_default();
 573
 574                                editor.update(cx, |editor, cx| {
 575                                    editor.set_text(summary, window, cx);
 576                                });
 577                            }
 578                        }
 579                        _ => {}
 580                    }
 581                }
 582            }),
 583            window.subscribe(&text_thread_editor.read(cx).text_thread().clone(), cx, {
 584                let editor = editor.clone();
 585                move |text_thread, event, window, cx| match event {
 586                    TextThreadEvent::SummaryGenerated => {
 587                        let summary = text_thread.read(cx).summary().or_default();
 588
 589                        editor.update(cx, |editor, cx| {
 590                            editor.set_text(summary, window, cx);
 591                        })
 592                    }
 593                    TextThreadEvent::PathChanged { .. } => {}
 594                    _ => {}
 595                }
 596            }),
 597        ];
 598
 599        let buffer_search_bar =
 600            cx.new(|cx| BufferSearchBar::new(Some(language_registry), window, cx));
 601        buffer_search_bar.update(cx, |buffer_search_bar, cx| {
 602            buffer_search_bar.set_active_pane_item(Some(&text_thread_editor), window, cx)
 603        });
 604
 605        Self::TextThread {
 606            text_thread_editor,
 607            title_editor: editor,
 608            buffer_search_bar,
 609            _subscriptions: subscriptions,
 610        }
 611    }
 612}
 613
 614pub struct AgentPanel {
 615    workspace: WeakEntity<Workspace>,
 616    /// Workspace id is used as a database key
 617    workspace_id: Option<WorkspaceId>,
 618    user_store: Entity<UserStore>,
 619    project: Entity<Project>,
 620    fs: Arc<dyn Fs>,
 621    language_registry: Arc<LanguageRegistry>,
 622    acp_history: Entity<ThreadHistory>,
 623    text_thread_history: Entity<TextThreadHistory>,
 624    thread_store: Entity<ThreadStore>,
 625    text_thread_store: Entity<assistant_text_thread::TextThreadStore>,
 626    prompt_store: Option<Entity<PromptStore>>,
 627    context_server_registry: Entity<ContextServerRegistry>,
 628    configuration: Option<Entity<AgentConfiguration>>,
 629    configuration_subscription: Option<Subscription>,
 630    focus_handle: FocusHandle,
 631    active_view: ActiveView,
 632    previous_view: Option<ActiveView>,
 633    background_threads: HashMap<acp::SessionId, Entity<ConnectionView>>,
 634    new_thread_menu_handle: PopoverMenuHandle<ContextMenu>,
 635    start_thread_in_menu_handle: PopoverMenuHandle<ContextMenu>,
 636    agent_panel_menu_handle: PopoverMenuHandle<ContextMenu>,
 637    agent_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
 638    agent_navigation_menu: Option<Entity<ContextMenu>>,
 639    _extension_subscription: Option<Subscription>,
 640    width: Option<Pixels>,
 641    height: Option<Pixels>,
 642    zoomed: bool,
 643    pending_serialization: Option<Task<Result<()>>>,
 644    onboarding: Entity<AgentPanelOnboarding>,
 645    selected_agent: AgentType,
 646    start_thread_in: StartThreadIn,
 647    worktree_creation_status: Option<WorktreeCreationStatus>,
 648    _thread_view_subscription: Option<Subscription>,
 649    _active_thread_focus_subscription: Option<Subscription>,
 650    _worktree_creation_task: Option<Task<()>>,
 651    show_trust_workspace_message: bool,
 652    last_configuration_error_telemetry: Option<String>,
 653    on_boarding_upsell_dismissed: AtomicBool,
 654    _active_view_observation: Option<Subscription>,
 655}
 656
 657impl AgentPanel {
 658    fn serialize(&mut self, cx: &mut App) {
 659        let Some(workspace_id) = self.workspace_id else {
 660            return;
 661        };
 662
 663        let width = self.width;
 664        let selected_agent = self.selected_agent.clone();
 665        let start_thread_in = Some(self.start_thread_in);
 666
 667        let last_active_thread = self.active_agent_thread(cx).map(|thread| {
 668            let thread = thread.read(cx);
 669            let title = thread.title();
 670            SerializedActiveThread {
 671                session_id: thread.session_id().0.to_string(),
 672                agent_type: self.selected_agent.clone(),
 673                title: if title.as_ref() != DEFAULT_THREAD_TITLE {
 674                    Some(title.to_string())
 675                } else {
 676                    None
 677                },
 678                cwd: None,
 679            }
 680        });
 681
 682        self.pending_serialization = Some(cx.background_spawn(async move {
 683            save_serialized_panel(
 684                workspace_id,
 685                SerializedAgentPanel {
 686                    width,
 687                    selected_agent: Some(selected_agent),
 688                    last_active_thread,
 689                    start_thread_in,
 690                },
 691            )
 692            .await?;
 693            anyhow::Ok(())
 694        }));
 695    }
 696
 697    pub fn load(
 698        workspace: WeakEntity<Workspace>,
 699        prompt_builder: Arc<PromptBuilder>,
 700        mut cx: AsyncWindowContext,
 701    ) -> Task<Result<Entity<Self>>> {
 702        let prompt_store = cx.update(|_window, cx| PromptStore::global(cx));
 703        cx.spawn(async move |cx| {
 704            let prompt_store = match prompt_store {
 705                Ok(prompt_store) => prompt_store.await.ok(),
 706                Err(_) => None,
 707            };
 708            let workspace_id = workspace
 709                .read_with(cx, |workspace, _| workspace.database_id())
 710                .ok()
 711                .flatten();
 712
 713            let serialized_panel = cx
 714                .background_spawn(async move {
 715                    workspace_id
 716                        .and_then(read_serialized_panel)
 717                        .or_else(read_legacy_serialized_panel)
 718                })
 719                .await;
 720
 721            let slash_commands = Arc::new(SlashCommandWorkingSet::default());
 722            let text_thread_store = workspace
 723                .update(cx, |workspace, cx| {
 724                    let project = workspace.project().clone();
 725                    assistant_text_thread::TextThreadStore::new(
 726                        project,
 727                        prompt_builder,
 728                        slash_commands,
 729                        cx,
 730                    )
 731                })?
 732                .await?;
 733
 734            let last_active_thread = if let Some(thread_info) = serialized_panel
 735                .as_ref()
 736                .and_then(|p| p.last_active_thread.clone())
 737            {
 738                if thread_info.agent_type.is_native() {
 739                    let session_id = acp::SessionId::new(thread_info.session_id.clone());
 740                    let load_result = cx.update(|_window, cx| {
 741                        let thread_store = ThreadStore::global(cx);
 742                        thread_store.update(cx, |store, cx| store.load_thread(session_id, cx))
 743                    });
 744                    let thread_exists = if let Ok(task) = load_result {
 745                        task.await.ok().flatten().is_some()
 746                    } else {
 747                        false
 748                    };
 749                    if thread_exists {
 750                        Some(thread_info)
 751                    } else {
 752                        log::warn!(
 753                            "last active thread {} not found in database, skipping restoration",
 754                            thread_info.session_id
 755                        );
 756                        None
 757                    }
 758                } else {
 759                    Some(thread_info)
 760                }
 761            } else {
 762                None
 763            };
 764
 765            let panel = workspace.update_in(cx, |workspace, window, cx| {
 766                let panel =
 767                    cx.new(|cx| Self::new(workspace, text_thread_store, prompt_store, window, cx));
 768
 769                if let Some(serialized_panel) = &serialized_panel {
 770                    panel.update(cx, |panel, cx| {
 771                        panel.width = serialized_panel.width.map(|w| w.round());
 772                        if let Some(selected_agent) = serialized_panel.selected_agent.clone() {
 773                            panel.selected_agent = selected_agent;
 774                        }
 775                        if let Some(start_thread_in) = serialized_panel.start_thread_in {
 776                            let is_worktree_flag_enabled =
 777                                cx.has_flag::<AgentV2FeatureFlag>();
 778                            let is_valid = match &start_thread_in {
 779                                StartThreadIn::LocalProject => true,
 780                                StartThreadIn::NewWorktree => {
 781                                    let project = panel.project.read(cx);
 782                                    is_worktree_flag_enabled && !project.is_via_collab()
 783                                }
 784                            };
 785                            if is_valid {
 786                                panel.start_thread_in = start_thread_in;
 787                            } else {
 788                                log::info!(
 789                                    "deserialized start_thread_in {:?} is no longer valid, falling back to LocalProject",
 790                                    start_thread_in,
 791                                );
 792                            }
 793                        }
 794                        cx.notify();
 795                    });
 796                }
 797
 798                if let Some(thread_info) = last_active_thread {
 799                    let agent_type = thread_info.agent_type.clone();
 800                    panel.update(cx, |panel, cx| {
 801                        panel.selected_agent = agent_type;
 802                        panel.load_agent_thread_inner(thread_info.session_id.into(), thread_info.cwd, thread_info.title.map(SharedString::from), false, window, cx);
 803                    });
 804                }
 805                panel
 806            })?;
 807
 808            Ok(panel)
 809        })
 810    }
 811
 812    pub(crate) fn new(
 813        workspace: &Workspace,
 814        text_thread_store: Entity<assistant_text_thread::TextThreadStore>,
 815        prompt_store: Option<Entity<PromptStore>>,
 816        window: &mut Window,
 817        cx: &mut Context<Self>,
 818    ) -> Self {
 819        let fs = workspace.app_state().fs.clone();
 820        let user_store = workspace.app_state().user_store.clone();
 821        let project = workspace.project();
 822        let language_registry = project.read(cx).languages().clone();
 823        let client = workspace.client().clone();
 824        let workspace_id = workspace.database_id();
 825        let workspace = workspace.weak_handle();
 826
 827        let context_server_registry =
 828            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
 829
 830        let thread_store = ThreadStore::global(cx);
 831        let acp_history = cx.new(|cx| ThreadHistory::new(None, window, cx));
 832        let text_thread_history =
 833            cx.new(|cx| TextThreadHistory::new(text_thread_store.clone(), window, cx));
 834        cx.subscribe_in(
 835            &acp_history,
 836            window,
 837            |this, _, event, window, cx| match event {
 838                ThreadHistoryEvent::Open(thread) => {
 839                    this.load_agent_thread(
 840                        thread.session_id.clone(),
 841                        thread.cwd.clone(),
 842                        thread.title.clone(),
 843                        window,
 844                        cx,
 845                    );
 846                }
 847            },
 848        )
 849        .detach();
 850        cx.subscribe_in(
 851            &text_thread_history,
 852            window,
 853            |this, _, event, window, cx| match event {
 854                TextThreadHistoryEvent::Open(thread) => {
 855                    this.open_saved_text_thread(thread.path.clone(), window, cx)
 856                        .detach_and_log_err(cx);
 857                }
 858            },
 859        )
 860        .detach();
 861
 862        let active_view = ActiveView::Uninitialized;
 863
 864        let weak_panel = cx.entity().downgrade();
 865
 866        window.defer(cx, move |window, cx| {
 867            let panel = weak_panel.clone();
 868            let agent_navigation_menu =
 869                ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| {
 870                    if let Some(panel) = panel.upgrade() {
 871                        if let Some(kind) = panel.read(cx).history_kind_for_selected_agent(cx) {
 872                            menu =
 873                                Self::populate_recently_updated_menu_section(menu, panel, kind, cx);
 874                            let view_all_label = match kind {
 875                                HistoryKind::AgentThreads => "View All",
 876                                HistoryKind::TextThreads => "View All Text Threads",
 877                            };
 878                            menu = menu.action(view_all_label, Box::new(OpenHistory));
 879                        }
 880                    }
 881
 882                    menu = menu
 883                        .fixed_width(px(320.).into())
 884                        .keep_open_on_confirm(false)
 885                        .key_context("NavigationMenu");
 886
 887                    menu
 888                });
 889            weak_panel
 890                .update(cx, |panel, cx| {
 891                    cx.subscribe_in(
 892                        &agent_navigation_menu,
 893                        window,
 894                        |_, menu, _: &DismissEvent, window, cx| {
 895                            menu.update(cx, |menu, _| {
 896                                menu.clear_selected();
 897                            });
 898                            cx.focus_self(window);
 899                        },
 900                    )
 901                    .detach();
 902                    panel.agent_navigation_menu = Some(agent_navigation_menu);
 903                })
 904                .ok();
 905        });
 906
 907        let weak_panel = cx.entity().downgrade();
 908        let onboarding = cx.new(|cx| {
 909            AgentPanelOnboarding::new(
 910                user_store.clone(),
 911                client,
 912                move |_window, cx| {
 913                    weak_panel
 914                        .update(cx, |panel, _| {
 915                            panel
 916                                .on_boarding_upsell_dismissed
 917                                .store(true, Ordering::Release);
 918                        })
 919                        .ok();
 920                    OnboardingUpsell::set_dismissed(true, cx);
 921                },
 922                cx,
 923            )
 924        });
 925
 926        // Subscribe to extension events to sync agent servers when extensions change
 927        let extension_subscription = if let Some(extension_events) = ExtensionEvents::try_global(cx)
 928        {
 929            Some(
 930                cx.subscribe(&extension_events, |this, _source, event, cx| match event {
 931                    extension::Event::ExtensionInstalled(_)
 932                    | extension::Event::ExtensionUninstalled(_)
 933                    | extension::Event::ExtensionsInstalledChanged => {
 934                        this.sync_agent_servers_from_extensions(cx);
 935                    }
 936                    _ => {}
 937                }),
 938            )
 939        } else {
 940            None
 941        };
 942
 943        let mut panel = Self {
 944            workspace_id,
 945            active_view,
 946            workspace,
 947            user_store,
 948            project: project.clone(),
 949            fs: fs.clone(),
 950            language_registry,
 951            text_thread_store,
 952            prompt_store,
 953            configuration: None,
 954            configuration_subscription: None,
 955            focus_handle: cx.focus_handle(),
 956            context_server_registry,
 957            previous_view: None,
 958            background_threads: HashMap::default(),
 959            new_thread_menu_handle: PopoverMenuHandle::default(),
 960            start_thread_in_menu_handle: PopoverMenuHandle::default(),
 961            agent_panel_menu_handle: PopoverMenuHandle::default(),
 962            agent_navigation_menu_handle: PopoverMenuHandle::default(),
 963            agent_navigation_menu: None,
 964            _extension_subscription: extension_subscription,
 965            width: None,
 966            height: None,
 967            zoomed: false,
 968            pending_serialization: None,
 969            onboarding,
 970            acp_history,
 971            text_thread_history,
 972            thread_store,
 973            selected_agent: AgentType::default(),
 974            start_thread_in: StartThreadIn::default(),
 975            worktree_creation_status: None,
 976            _thread_view_subscription: None,
 977            _active_thread_focus_subscription: None,
 978            _worktree_creation_task: None,
 979            show_trust_workspace_message: false,
 980            last_configuration_error_telemetry: None,
 981            on_boarding_upsell_dismissed: AtomicBool::new(OnboardingUpsell::dismissed()),
 982            _active_view_observation: None,
 983        };
 984
 985        // Initial sync of agent servers from extensions
 986        panel.sync_agent_servers_from_extensions(cx);
 987        panel
 988    }
 989
 990    pub fn toggle_focus(
 991        workspace: &mut Workspace,
 992        _: &ToggleFocus,
 993        window: &mut Window,
 994        cx: &mut Context<Workspace>,
 995    ) {
 996        if workspace
 997            .panel::<Self>(cx)
 998            .is_some_and(|panel| panel.read(cx).enabled(cx))
 999        {
1000            workspace.toggle_panel_focus::<Self>(window, cx);
1001        }
1002    }
1003
1004    pub fn toggle(
1005        workspace: &mut Workspace,
1006        _: &Toggle,
1007        window: &mut Window,
1008        cx: &mut Context<Workspace>,
1009    ) {
1010        if workspace
1011            .panel::<Self>(cx)
1012            .is_some_and(|panel| panel.read(cx).enabled(cx))
1013        {
1014            if !workspace.toggle_panel_focus::<Self>(window, cx) {
1015                workspace.close_panel::<Self>(window, cx);
1016            }
1017        }
1018    }
1019
1020    pub(crate) fn prompt_store(&self) -> &Option<Entity<PromptStore>> {
1021        &self.prompt_store
1022    }
1023
1024    pub fn thread_store(&self) -> &Entity<ThreadStore> {
1025        &self.thread_store
1026    }
1027
1028    pub fn history(&self) -> &Entity<ThreadHistory> {
1029        &self.acp_history
1030    }
1031
1032    pub fn open_thread(
1033        &mut self,
1034        session_id: acp::SessionId,
1035        cwd: Option<PathBuf>,
1036        title: Option<SharedString>,
1037        window: &mut Window,
1038        cx: &mut Context<Self>,
1039    ) {
1040        self.external_thread(
1041            Some(crate::ExternalAgent::NativeAgent),
1042            Some(session_id),
1043            cwd,
1044            title,
1045            None,
1046            true,
1047            window,
1048            cx,
1049        );
1050    }
1051
1052    pub(crate) fn context_server_registry(&self) -> &Entity<ContextServerRegistry> {
1053        &self.context_server_registry
1054    }
1055
1056    pub fn is_visible(workspace: &Entity<Workspace>, cx: &App) -> bool {
1057        let workspace_read = workspace.read(cx);
1058
1059        workspace_read
1060            .panel::<AgentPanel>(cx)
1061            .map(|panel| {
1062                let panel_id = Entity::entity_id(&panel);
1063
1064                workspace_read.all_docks().iter().any(|dock| {
1065                    dock.read(cx)
1066                        .visible_panel()
1067                        .is_some_and(|visible_panel| visible_panel.panel_id() == panel_id)
1068                })
1069            })
1070            .unwrap_or(false)
1071    }
1072
1073    pub fn active_connection_view(&self) -> Option<&Entity<ConnectionView>> {
1074        match &self.active_view {
1075            ActiveView::AgentThread { server_view, .. } => Some(server_view),
1076            ActiveView::Uninitialized
1077            | ActiveView::TextThread { .. }
1078            | ActiveView::History { .. }
1079            | ActiveView::Configuration => None,
1080        }
1081    }
1082
1083    pub fn new_thread(&mut self, _action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
1084        self.new_agent_thread(AgentType::NativeAgent, window, cx);
1085    }
1086
1087    fn new_native_agent_thread_from_summary(
1088        &mut self,
1089        action: &NewNativeAgentThreadFromSummary,
1090        window: &mut Window,
1091        cx: &mut Context<Self>,
1092    ) {
1093        let Some(thread) = self
1094            .acp_history
1095            .read(cx)
1096            .session_for_id(&action.from_session_id)
1097        else {
1098            return;
1099        };
1100
1101        self.external_thread(
1102            Some(ExternalAgent::NativeAgent),
1103            None,
1104            None,
1105            None,
1106            Some(AgentInitialContent::ThreadSummary {
1107                session_id: thread.session_id,
1108                title: thread.title,
1109            }),
1110            true,
1111            window,
1112            cx,
1113        );
1114    }
1115
1116    fn new_text_thread(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1117        telemetry::event!("Agent Thread Started", agent = "zed-text");
1118
1119        let context = self
1120            .text_thread_store
1121            .update(cx, |context_store, cx| context_store.create(cx));
1122        let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
1123            .log_err()
1124            .flatten();
1125
1126        let text_thread_editor = cx.new(|cx| {
1127            let mut editor = TextThreadEditor::for_text_thread(
1128                context,
1129                self.fs.clone(),
1130                self.workspace.clone(),
1131                self.project.clone(),
1132                lsp_adapter_delegate,
1133                window,
1134                cx,
1135            );
1136            editor.insert_default_prompt(window, cx);
1137            editor
1138        });
1139
1140        if self.selected_agent != AgentType::TextThread {
1141            self.selected_agent = AgentType::TextThread;
1142            self.serialize(cx);
1143        }
1144
1145        self.set_active_view(
1146            ActiveView::text_thread(
1147                text_thread_editor.clone(),
1148                self.language_registry.clone(),
1149                window,
1150                cx,
1151            ),
1152            true,
1153            window,
1154            cx,
1155        );
1156        text_thread_editor.focus_handle(cx).focus(window, cx);
1157    }
1158
1159    fn external_thread(
1160        &mut self,
1161        agent_choice: Option<crate::ExternalAgent>,
1162        resume_session_id: Option<acp::SessionId>,
1163        cwd: Option<PathBuf>,
1164        title: Option<SharedString>,
1165        initial_content: Option<AgentInitialContent>,
1166        focus: bool,
1167        window: &mut Window,
1168        cx: &mut Context<Self>,
1169    ) {
1170        let workspace = self.workspace.clone();
1171        let project = self.project.clone();
1172        let fs = self.fs.clone();
1173        let is_via_collab = self.project.read(cx).is_via_collab();
1174
1175        const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent";
1176
1177        #[derive(Serialize, Deserialize)]
1178        struct LastUsedExternalAgent {
1179            agent: crate::ExternalAgent,
1180        }
1181
1182        let thread_store = self.thread_store.clone();
1183
1184        if let Some(agent) = agent_choice {
1185            cx.background_spawn({
1186                let agent = agent.clone();
1187                async move {
1188                    if let Some(serialized) =
1189                        serde_json::to_string(&LastUsedExternalAgent { agent }).log_err()
1190                    {
1191                        KEY_VALUE_STORE
1192                            .write_kvp(LAST_USED_EXTERNAL_AGENT_KEY.to_string(), serialized)
1193                            .await
1194                            .log_err();
1195                    }
1196                }
1197            })
1198            .detach();
1199
1200            let server = agent.server(fs, thread_store);
1201            self.create_external_thread(
1202                server,
1203                resume_session_id,
1204                cwd,
1205                title,
1206                initial_content,
1207                workspace,
1208                project,
1209                agent,
1210                focus,
1211                window,
1212                cx,
1213            );
1214        } else {
1215            cx.spawn_in(window, async move |this, cx| {
1216                let ext_agent = if is_via_collab {
1217                    ExternalAgent::NativeAgent
1218                } else {
1219                    cx.background_spawn(async move {
1220                        KEY_VALUE_STORE.read_kvp(LAST_USED_EXTERNAL_AGENT_KEY)
1221                    })
1222                    .await
1223                    .log_err()
1224                    .flatten()
1225                    .and_then(|value| {
1226                        serde_json::from_str::<LastUsedExternalAgent>(&value).log_err()
1227                    })
1228                    .map(|agent| agent.agent)
1229                    .unwrap_or(ExternalAgent::NativeAgent)
1230                };
1231
1232                let server = ext_agent.server(fs, thread_store);
1233                this.update_in(cx, |agent_panel, window, cx| {
1234                    agent_panel.create_external_thread(
1235                        server,
1236                        resume_session_id,
1237                        cwd,
1238                        title,
1239                        initial_content,
1240                        workspace,
1241                        project,
1242                        ext_agent,
1243                        focus,
1244                        window,
1245                        cx,
1246                    );
1247                })?;
1248
1249                anyhow::Ok(())
1250            })
1251            .detach_and_log_err(cx);
1252        }
1253    }
1254
1255    fn deploy_rules_library(
1256        &mut self,
1257        action: &OpenRulesLibrary,
1258        _window: &mut Window,
1259        cx: &mut Context<Self>,
1260    ) {
1261        open_rules_library(
1262            self.language_registry.clone(),
1263            Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())),
1264            Rc::new(|| {
1265                Rc::new(SlashCommandCompletionProvider::new(
1266                    Arc::new(SlashCommandWorkingSet::default()),
1267                    None,
1268                    None,
1269                ))
1270            }),
1271            action
1272                .prompt_to_select
1273                .map(|uuid| UserPromptId(uuid).into()),
1274            cx,
1275        )
1276        .detach_and_log_err(cx);
1277    }
1278
1279    fn expand_message_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1280        let Some(thread_view) = self.active_connection_view() else {
1281            return;
1282        };
1283
1284        let Some(active_thread) = thread_view.read(cx).active_thread().cloned() else {
1285            return;
1286        };
1287
1288        active_thread.update(cx, |active_thread, cx| {
1289            active_thread.expand_message_editor(&ExpandMessageEditor, window, cx);
1290            active_thread.focus_handle(cx).focus(window, cx);
1291        })
1292    }
1293
1294    fn history_kind_for_selected_agent(&self, cx: &App) -> Option<HistoryKind> {
1295        match self.selected_agent {
1296            AgentType::NativeAgent => Some(HistoryKind::AgentThreads),
1297            AgentType::TextThread => Some(HistoryKind::TextThreads),
1298            AgentType::Custom { .. } => {
1299                if self.acp_history.read(cx).has_session_list() {
1300                    Some(HistoryKind::AgentThreads)
1301                } else {
1302                    None
1303                }
1304            }
1305        }
1306    }
1307
1308    fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1309        let Some(kind) = self.history_kind_for_selected_agent(cx) else {
1310            return;
1311        };
1312
1313        if let ActiveView::History { kind: active_kind } = self.active_view {
1314            if active_kind == kind {
1315                if let Some(previous_view) = self.previous_view.take() {
1316                    self.set_active_view(previous_view, true, window, cx);
1317                }
1318                return;
1319            }
1320        }
1321
1322        self.set_active_view(ActiveView::History { kind }, true, window, cx);
1323        cx.notify();
1324    }
1325
1326    pub(crate) fn open_saved_text_thread(
1327        &mut self,
1328        path: Arc<Path>,
1329        window: &mut Window,
1330        cx: &mut Context<Self>,
1331    ) -> Task<Result<()>> {
1332        let text_thread_task = self
1333            .text_thread_store
1334            .update(cx, |store, cx| store.open_local(path, cx));
1335        cx.spawn_in(window, async move |this, cx| {
1336            let text_thread = text_thread_task.await?;
1337            this.update_in(cx, |this, window, cx| {
1338                this.open_text_thread(text_thread, window, cx);
1339            })
1340        })
1341    }
1342
1343    pub(crate) fn open_text_thread(
1344        &mut self,
1345        text_thread: Entity<TextThread>,
1346        window: &mut Window,
1347        cx: &mut Context<Self>,
1348    ) {
1349        let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project.clone(), cx)
1350            .log_err()
1351            .flatten();
1352        let editor = cx.new(|cx| {
1353            TextThreadEditor::for_text_thread(
1354                text_thread,
1355                self.fs.clone(),
1356                self.workspace.clone(),
1357                self.project.clone(),
1358                lsp_adapter_delegate,
1359                window,
1360                cx,
1361            )
1362        });
1363
1364        if self.selected_agent != AgentType::TextThread {
1365            self.selected_agent = AgentType::TextThread;
1366            self.serialize(cx);
1367        }
1368
1369        self.set_active_view(
1370            ActiveView::text_thread(editor, self.language_registry.clone(), window, cx),
1371            true,
1372            window,
1373            cx,
1374        );
1375    }
1376
1377    pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
1378        match self.active_view {
1379            ActiveView::Configuration | ActiveView::History { .. } => {
1380                if let Some(previous_view) = self.previous_view.take() {
1381                    self.set_active_view(previous_view, true, window, cx);
1382                }
1383                cx.notify();
1384            }
1385            _ => {}
1386        }
1387    }
1388
1389    pub fn toggle_navigation_menu(
1390        &mut self,
1391        _: &ToggleNavigationMenu,
1392        window: &mut Window,
1393        cx: &mut Context<Self>,
1394    ) {
1395        if self.history_kind_for_selected_agent(cx).is_none() {
1396            return;
1397        }
1398        self.agent_navigation_menu_handle.toggle(window, cx);
1399    }
1400
1401    pub fn toggle_options_menu(
1402        &mut self,
1403        _: &ToggleOptionsMenu,
1404        window: &mut Window,
1405        cx: &mut Context<Self>,
1406    ) {
1407        self.agent_panel_menu_handle.toggle(window, cx);
1408    }
1409
1410    pub fn toggle_new_thread_menu(
1411        &mut self,
1412        _: &ToggleNewThreadMenu,
1413        window: &mut Window,
1414        cx: &mut Context<Self>,
1415    ) {
1416        self.new_thread_menu_handle.toggle(window, cx);
1417    }
1418
1419    pub fn toggle_start_thread_in_selector(
1420        &mut self,
1421        _: &ToggleStartThreadInSelector,
1422        window: &mut Window,
1423        cx: &mut Context<Self>,
1424    ) {
1425        self.start_thread_in_menu_handle.toggle(window, cx);
1426    }
1427
1428    pub fn increase_font_size(
1429        &mut self,
1430        action: &IncreaseBufferFontSize,
1431        _: &mut Window,
1432        cx: &mut Context<Self>,
1433    ) {
1434        self.handle_font_size_action(action.persist, px(1.0), cx);
1435    }
1436
1437    pub fn decrease_font_size(
1438        &mut self,
1439        action: &DecreaseBufferFontSize,
1440        _: &mut Window,
1441        cx: &mut Context<Self>,
1442    ) {
1443        self.handle_font_size_action(action.persist, px(-1.0), cx);
1444    }
1445
1446    fn handle_font_size_action(&mut self, persist: bool, delta: Pixels, cx: &mut Context<Self>) {
1447        match self.active_view.which_font_size_used() {
1448            WhichFontSize::AgentFont => {
1449                if persist {
1450                    update_settings_file(self.fs.clone(), cx, move |settings, cx| {
1451                        let agent_ui_font_size =
1452                            ThemeSettings::get_global(cx).agent_ui_font_size(cx) + delta;
1453                        let agent_buffer_font_size =
1454                            ThemeSettings::get_global(cx).agent_buffer_font_size(cx) + delta;
1455
1456                        let _ = settings
1457                            .theme
1458                            .agent_ui_font_size
1459                            .insert(f32::from(theme::clamp_font_size(agent_ui_font_size)).into());
1460                        let _ = settings.theme.agent_buffer_font_size.insert(
1461                            f32::from(theme::clamp_font_size(agent_buffer_font_size)).into(),
1462                        );
1463                    });
1464                } else {
1465                    theme::adjust_agent_ui_font_size(cx, |size| size + delta);
1466                    theme::adjust_agent_buffer_font_size(cx, |size| size + delta);
1467                }
1468            }
1469            WhichFontSize::BufferFont => {
1470                // Prompt editor uses the buffer font size, so allow the action to propagate to the
1471                // default handler that changes that font size.
1472                cx.propagate();
1473            }
1474            WhichFontSize::None => {}
1475        }
1476    }
1477
1478    pub fn reset_font_size(
1479        &mut self,
1480        action: &ResetBufferFontSize,
1481        _: &mut Window,
1482        cx: &mut Context<Self>,
1483    ) {
1484        if action.persist {
1485            update_settings_file(self.fs.clone(), cx, move |settings, _| {
1486                settings.theme.agent_ui_font_size = None;
1487                settings.theme.agent_buffer_font_size = None;
1488            });
1489        } else {
1490            theme::reset_agent_ui_font_size(cx);
1491            theme::reset_agent_buffer_font_size(cx);
1492        }
1493    }
1494
1495    pub fn reset_agent_zoom(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
1496        theme::reset_agent_ui_font_size(cx);
1497        theme::reset_agent_buffer_font_size(cx);
1498    }
1499
1500    pub fn toggle_zoom(&mut self, _: &ToggleZoom, window: &mut Window, cx: &mut Context<Self>) {
1501        if self.zoomed {
1502            cx.emit(PanelEvent::ZoomOut);
1503        } else {
1504            if !self.focus_handle(cx).contains_focused(window, cx) {
1505                cx.focus_self(window);
1506            }
1507            cx.emit(PanelEvent::ZoomIn);
1508        }
1509    }
1510
1511    pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1512        let agent_server_store = self.project.read(cx).agent_server_store().clone();
1513        let context_server_store = self.project.read(cx).context_server_store();
1514        let fs = self.fs.clone();
1515
1516        self.set_active_view(ActiveView::Configuration, true, window, cx);
1517        self.configuration = Some(cx.new(|cx| {
1518            AgentConfiguration::new(
1519                fs,
1520                agent_server_store,
1521                context_server_store,
1522                self.context_server_registry.clone(),
1523                self.language_registry.clone(),
1524                self.workspace.clone(),
1525                window,
1526                cx,
1527            )
1528        }));
1529
1530        if let Some(configuration) = self.configuration.as_ref() {
1531            self.configuration_subscription = Some(cx.subscribe_in(
1532                configuration,
1533                window,
1534                Self::handle_agent_configuration_event,
1535            ));
1536
1537            configuration.focus_handle(cx).focus(window, cx);
1538        }
1539    }
1540
1541    pub(crate) fn open_active_thread_as_markdown(
1542        &mut self,
1543        _: &OpenActiveThreadAsMarkdown,
1544        window: &mut Window,
1545        cx: &mut Context<Self>,
1546    ) {
1547        if let Some(workspace) = self.workspace.upgrade()
1548            && let Some(thread_view) = self.active_connection_view()
1549            && let Some(active_thread) = thread_view.read(cx).active_thread().cloned()
1550        {
1551            active_thread.update(cx, |thread, cx| {
1552                thread
1553                    .open_thread_as_markdown(workspace, window, cx)
1554                    .detach_and_log_err(cx);
1555            });
1556        }
1557    }
1558
1559    fn copy_thread_to_clipboard(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1560        let Some(thread) = self.active_native_agent_thread(cx) else {
1561            Self::show_deferred_toast(&self.workspace, "No active native thread to copy", cx);
1562            return;
1563        };
1564
1565        let workspace = self.workspace.clone();
1566        let load_task = thread.read(cx).to_db(cx);
1567
1568        cx.spawn_in(window, async move |_this, cx| {
1569            let db_thread = load_task.await;
1570            let shared_thread = SharedThread::from_db_thread(&db_thread);
1571            let thread_data = shared_thread.to_bytes()?;
1572            let encoded = base64::Engine::encode(&base64::prelude::BASE64_STANDARD, &thread_data);
1573
1574            cx.update(|_window, cx| {
1575                cx.write_to_clipboard(ClipboardItem::new_string(encoded));
1576                if let Some(workspace) = workspace.upgrade() {
1577                    workspace.update(cx, |workspace, cx| {
1578                        struct ThreadCopiedToast;
1579                        workspace.show_toast(
1580                            workspace::Toast::new(
1581                                workspace::notifications::NotificationId::unique::<ThreadCopiedToast>(),
1582                                "Thread copied to clipboard (base64 encoded)",
1583                            )
1584                            .autohide(),
1585                            cx,
1586                        );
1587                    });
1588                }
1589            })?;
1590
1591            anyhow::Ok(())
1592        })
1593        .detach_and_log_err(cx);
1594    }
1595
1596    fn show_deferred_toast(
1597        workspace: &WeakEntity<workspace::Workspace>,
1598        message: &'static str,
1599        cx: &mut App,
1600    ) {
1601        let workspace = workspace.clone();
1602        cx.defer(move |cx| {
1603            if let Some(workspace) = workspace.upgrade() {
1604                workspace.update(cx, |workspace, cx| {
1605                    struct ClipboardToast;
1606                    workspace.show_toast(
1607                        workspace::Toast::new(
1608                            workspace::notifications::NotificationId::unique::<ClipboardToast>(),
1609                            message,
1610                        )
1611                        .autohide(),
1612                        cx,
1613                    );
1614                });
1615            }
1616        });
1617    }
1618
1619    fn load_thread_from_clipboard(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1620        let Some(clipboard) = cx.read_from_clipboard() else {
1621            Self::show_deferred_toast(&self.workspace, "No clipboard content available", cx);
1622            return;
1623        };
1624
1625        let Some(encoded) = clipboard.text() else {
1626            Self::show_deferred_toast(&self.workspace, "Clipboard does not contain text", cx);
1627            return;
1628        };
1629
1630        let thread_data = match base64::Engine::decode(&base64::prelude::BASE64_STANDARD, &encoded)
1631        {
1632            Ok(data) => data,
1633            Err(_) => {
1634                Self::show_deferred_toast(
1635                    &self.workspace,
1636                    "Failed to decode clipboard content (expected base64)",
1637                    cx,
1638                );
1639                return;
1640            }
1641        };
1642
1643        let shared_thread = match SharedThread::from_bytes(&thread_data) {
1644            Ok(thread) => thread,
1645            Err(_) => {
1646                Self::show_deferred_toast(
1647                    &self.workspace,
1648                    "Failed to parse thread data from clipboard",
1649                    cx,
1650                );
1651                return;
1652            }
1653        };
1654
1655        let db_thread = shared_thread.to_db_thread();
1656        let session_id = acp::SessionId::new(uuid::Uuid::new_v4().to_string());
1657        let thread_store = self.thread_store.clone();
1658        let title = db_thread.title.clone();
1659        let workspace = self.workspace.clone();
1660
1661        cx.spawn_in(window, async move |this, cx| {
1662            thread_store
1663                .update(&mut cx.clone(), |store, cx| {
1664                    store.save_thread(session_id.clone(), db_thread, Default::default(), cx)
1665                })
1666                .await?;
1667
1668            this.update_in(cx, |this, window, cx| {
1669                this.open_thread(session_id, None, Some(title), window, cx);
1670            })?;
1671
1672            this.update_in(cx, |_, _window, cx| {
1673                if let Some(workspace) = workspace.upgrade() {
1674                    workspace.update(cx, |workspace, cx| {
1675                        struct ThreadLoadedToast;
1676                        workspace.show_toast(
1677                            workspace::Toast::new(
1678                                workspace::notifications::NotificationId::unique::<ThreadLoadedToast>(),
1679                                "Thread loaded from clipboard",
1680                            )
1681                            .autohide(),
1682                            cx,
1683                        );
1684                    });
1685                }
1686            })?;
1687
1688            anyhow::Ok(())
1689        })
1690        .detach_and_log_err(cx);
1691    }
1692
1693    fn handle_agent_configuration_event(
1694        &mut self,
1695        _entity: &Entity<AgentConfiguration>,
1696        event: &AssistantConfigurationEvent,
1697        window: &mut Window,
1698        cx: &mut Context<Self>,
1699    ) {
1700        match event {
1701            AssistantConfigurationEvent::NewThread(provider) => {
1702                if LanguageModelRegistry::read_global(cx)
1703                    .default_model()
1704                    .is_none_or(|model| model.provider.id() != provider.id())
1705                    && let Some(model) = provider.default_model(cx)
1706                {
1707                    update_settings_file(self.fs.clone(), cx, move |settings, _| {
1708                        let provider = model.provider_id().0.to_string();
1709                        let enable_thinking = model.supports_thinking();
1710                        let effort = model
1711                            .default_effort_level()
1712                            .map(|effort| effort.value.to_string());
1713                        let model = model.id().0.to_string();
1714                        settings
1715                            .agent
1716                            .get_or_insert_default()
1717                            .set_model(LanguageModelSelection {
1718                                provider: LanguageModelProviderSetting(provider),
1719                                model,
1720                                enable_thinking,
1721                                effort,
1722                            })
1723                    });
1724                }
1725
1726                self.new_thread(&NewThread, window, cx);
1727                if let Some((thread, model)) = self
1728                    .active_native_agent_thread(cx)
1729                    .zip(provider.default_model(cx))
1730                {
1731                    thread.update(cx, |thread, cx| {
1732                        thread.set_model(model, cx);
1733                    });
1734                }
1735            }
1736        }
1737    }
1738
1739    pub fn as_active_server_view(&self) -> Option<&Entity<ConnectionView>> {
1740        match &self.active_view {
1741            ActiveView::AgentThread { server_view } => Some(server_view),
1742            _ => None,
1743        }
1744    }
1745
1746    pub fn as_active_thread_view(&self, cx: &App) -> Option<Entity<ThreadView>> {
1747        let server_view = self.as_active_server_view()?;
1748        server_view.read(cx).active_thread().cloned()
1749    }
1750
1751    pub fn active_agent_thread(&self, cx: &App) -> Option<Entity<AcpThread>> {
1752        match &self.active_view {
1753            ActiveView::AgentThread { server_view, .. } => server_view
1754                .read(cx)
1755                .active_thread()
1756                .map(|r| r.read(cx).thread.clone()),
1757            _ => None,
1758        }
1759    }
1760
1761    /// Returns the primary thread views for all retained connections: the
1762    pub fn is_background_thread(&self, session_id: &acp::SessionId) -> bool {
1763        self.background_threads.contains_key(session_id)
1764    }
1765
1766    /// active thread plus any background threads that are still running or
1767    /// completed but unseen.
1768    pub fn parent_threads(&self, cx: &App) -> Vec<Entity<ThreadView>> {
1769        let mut views = Vec::new();
1770
1771        if let Some(server_view) = self.as_active_server_view() {
1772            if let Some(thread_view) = server_view.read(cx).parent_thread(cx) {
1773                views.push(thread_view);
1774            }
1775        }
1776
1777        for server_view in self.background_threads.values() {
1778            if let Some(thread_view) = server_view.read(cx).parent_thread(cx) {
1779                views.push(thread_view);
1780            }
1781        }
1782
1783        views
1784    }
1785
1786    fn retain_running_thread(&mut self, old_view: ActiveView, cx: &mut Context<Self>) {
1787        let ActiveView::AgentThread { server_view } = old_view else {
1788            return;
1789        };
1790
1791        let Some(thread_view) = server_view.read(cx).parent_thread(cx) else {
1792            return;
1793        };
1794
1795        let thread = &thread_view.read(cx).thread;
1796        let (status, session_id) = {
1797            let thread = thread.read(cx);
1798            (thread.status(), thread.session_id().clone())
1799        };
1800
1801        if status != ThreadStatus::Generating {
1802            return;
1803        }
1804
1805        self.background_threads.insert(session_id, server_view);
1806    }
1807
1808    pub(crate) fn active_native_agent_thread(&self, cx: &App) -> Option<Entity<agent::Thread>> {
1809        match &self.active_view {
1810            ActiveView::AgentThread { server_view, .. } => {
1811                server_view.read(cx).as_native_thread(cx)
1812            }
1813            _ => None,
1814        }
1815    }
1816
1817    pub(crate) fn active_text_thread_editor(&self) -> Option<Entity<TextThreadEditor>> {
1818        match &self.active_view {
1819            ActiveView::TextThread {
1820                text_thread_editor, ..
1821            } => Some(text_thread_editor.clone()),
1822            _ => None,
1823        }
1824    }
1825
1826    fn set_active_view(
1827        &mut self,
1828        new_view: ActiveView,
1829        focus: bool,
1830        window: &mut Window,
1831        cx: &mut Context<Self>,
1832    ) {
1833        let was_in_agent_history = matches!(
1834            self.active_view,
1835            ActiveView::History {
1836                kind: HistoryKind::AgentThreads
1837            }
1838        );
1839        let current_is_uninitialized = matches!(self.active_view, ActiveView::Uninitialized);
1840        let current_is_history = matches!(self.active_view, ActiveView::History { .. });
1841        let new_is_history = matches!(new_view, ActiveView::History { .. });
1842
1843        let current_is_config = matches!(self.active_view, ActiveView::Configuration);
1844        let new_is_config = matches!(new_view, ActiveView::Configuration);
1845
1846        let current_is_overlay = current_is_history || current_is_config;
1847        let new_is_overlay = new_is_history || new_is_config;
1848
1849        if current_is_uninitialized || (current_is_overlay && !new_is_overlay) {
1850            self.active_view = new_view;
1851        } else if !current_is_overlay && new_is_overlay {
1852            self.previous_view = Some(std::mem::replace(&mut self.active_view, new_view));
1853        } else {
1854            let old_view = std::mem::replace(&mut self.active_view, new_view);
1855            if !new_is_overlay {
1856                if let Some(previous) = self.previous_view.take() {
1857                    self.retain_running_thread(previous, cx);
1858                }
1859            }
1860            self.retain_running_thread(old_view, cx);
1861        }
1862
1863        // Subscribe to the active ThreadView's events (e.g. FirstSendRequested)
1864        // so the panel can intercept the first send for worktree creation.
1865        // Re-subscribe whenever the ConnectionView changes, since the inner
1866        // ThreadView may have been replaced (e.g. navigating between threads).
1867        self._active_view_observation = match &self.active_view {
1868            ActiveView::AgentThread { server_view } => {
1869                self._thread_view_subscription =
1870                    Self::subscribe_to_active_thread_view(server_view, window, cx);
1871                let focus_handle = server_view.focus_handle(cx);
1872                self._active_thread_focus_subscription =
1873                    Some(cx.on_focus_in(&focus_handle, window, |_this, _window, cx| {
1874                        cx.emit(AgentPanelEvent::ThreadFocused);
1875                        cx.notify();
1876                    }));
1877                Some(
1878                    cx.observe_in(server_view, window, |this, server_view, window, cx| {
1879                        this._thread_view_subscription =
1880                            Self::subscribe_to_active_thread_view(&server_view, window, cx);
1881                        cx.emit(AgentPanelEvent::ActiveViewChanged);
1882                        this.serialize(cx);
1883                        cx.notify();
1884                    }),
1885                )
1886            }
1887            _ => {
1888                self._thread_view_subscription = None;
1889                self._active_thread_focus_subscription = None;
1890                None
1891            }
1892        };
1893
1894        let is_in_agent_history = matches!(
1895            self.active_view,
1896            ActiveView::History {
1897                kind: HistoryKind::AgentThreads
1898            }
1899        );
1900
1901        if !was_in_agent_history && is_in_agent_history {
1902            self.acp_history
1903                .update(cx, |history, cx| history.refresh_full_history(cx));
1904        }
1905
1906        if focus {
1907            self.focus_handle(cx).focus(window, cx);
1908        }
1909        cx.emit(AgentPanelEvent::ActiveViewChanged);
1910    }
1911
1912    fn populate_recently_updated_menu_section(
1913        mut menu: ContextMenu,
1914        panel: Entity<Self>,
1915        kind: HistoryKind,
1916        cx: &mut Context<ContextMenu>,
1917    ) -> ContextMenu {
1918        match kind {
1919            HistoryKind::AgentThreads => {
1920                let entries = panel
1921                    .read(cx)
1922                    .acp_history
1923                    .read(cx)
1924                    .sessions()
1925                    .iter()
1926                    .take(RECENTLY_UPDATED_MENU_LIMIT)
1927                    .cloned()
1928                    .collect::<Vec<_>>();
1929
1930                if entries.is_empty() {
1931                    return menu;
1932                }
1933
1934                menu = menu.header("Recently Updated");
1935
1936                for entry in entries {
1937                    let title = entry
1938                        .title
1939                        .as_ref()
1940                        .filter(|title| !title.is_empty())
1941                        .cloned()
1942                        .unwrap_or_else(|| SharedString::new_static(DEFAULT_THREAD_TITLE));
1943
1944                    menu = menu.entry(title, None, {
1945                        let panel = panel.downgrade();
1946                        let entry = entry.clone();
1947                        move |window, cx| {
1948                            let entry = entry.clone();
1949                            panel
1950                                .update(cx, move |this, cx| {
1951                                    this.load_agent_thread(
1952                                        entry.session_id.clone(),
1953                                        entry.cwd.clone(),
1954                                        entry.title.clone(),
1955                                        window,
1956                                        cx,
1957                                    );
1958                                })
1959                                .ok();
1960                        }
1961                    });
1962                }
1963            }
1964            HistoryKind::TextThreads => {
1965                let entries = panel
1966                    .read(cx)
1967                    .text_thread_store
1968                    .read(cx)
1969                    .ordered_text_threads()
1970                    .take(RECENTLY_UPDATED_MENU_LIMIT)
1971                    .cloned()
1972                    .collect::<Vec<_>>();
1973
1974                if entries.is_empty() {
1975                    return menu;
1976                }
1977
1978                menu = menu.header("Recent Text Threads");
1979
1980                for entry in entries {
1981                    let title = if entry.title.is_empty() {
1982                        SharedString::new_static(DEFAULT_THREAD_TITLE)
1983                    } else {
1984                        entry.title.clone()
1985                    };
1986
1987                    menu = menu.entry(title, None, {
1988                        let panel = panel.downgrade();
1989                        let entry = entry.clone();
1990                        move |window, cx| {
1991                            let path = entry.path.clone();
1992                            panel
1993                                .update(cx, move |this, cx| {
1994                                    this.open_saved_text_thread(path.clone(), window, cx)
1995                                        .detach_and_log_err(cx);
1996                                })
1997                                .ok();
1998                        }
1999                    });
2000                }
2001            }
2002        }
2003
2004        menu.separator()
2005    }
2006
2007    pub fn selected_agent(&self) -> AgentType {
2008        self.selected_agent.clone()
2009    }
2010
2011    fn subscribe_to_active_thread_view(
2012        server_view: &Entity<ConnectionView>,
2013        window: &mut Window,
2014        cx: &mut Context<Self>,
2015    ) -> Option<Subscription> {
2016        server_view.read(cx).active_thread().cloned().map(|tv| {
2017            cx.subscribe_in(
2018                &tv,
2019                window,
2020                |this, view, event: &AcpThreadViewEvent, window, cx| match event {
2021                    AcpThreadViewEvent::FirstSendRequested { content } => {
2022                        this.handle_first_send_requested(view.clone(), content.clone(), window, cx);
2023                    }
2024                },
2025            )
2026        })
2027    }
2028
2029    pub fn start_thread_in(&self) -> &StartThreadIn {
2030        &self.start_thread_in
2031    }
2032
2033    fn set_start_thread_in(&mut self, action: &StartThreadIn, cx: &mut Context<Self>) {
2034        if matches!(action, StartThreadIn::NewWorktree) && !cx.has_flag::<AgentV2FeatureFlag>() {
2035            return;
2036        }
2037
2038        let new_target = match *action {
2039            StartThreadIn::LocalProject => StartThreadIn::LocalProject,
2040            StartThreadIn::NewWorktree => {
2041                if !self.project_has_git_repository(cx) {
2042                    log::error!(
2043                        "set_start_thread_in: cannot use NewWorktree without a git repository"
2044                    );
2045                    return;
2046                }
2047                if self.project.read(cx).is_via_collab() {
2048                    log::error!("set_start_thread_in: cannot use NewWorktree in a collab project");
2049                    return;
2050                }
2051                StartThreadIn::NewWorktree
2052            }
2053        };
2054        self.start_thread_in = new_target;
2055        self.serialize(cx);
2056        cx.notify();
2057    }
2058
2059    fn selected_external_agent(&self) -> Option<ExternalAgent> {
2060        match &self.selected_agent {
2061            AgentType::NativeAgent => Some(ExternalAgent::NativeAgent),
2062            AgentType::Custom { name } => Some(ExternalAgent::Custom { name: name.clone() }),
2063            AgentType::TextThread => None,
2064        }
2065    }
2066
2067    fn sync_agent_servers_from_extensions(&mut self, cx: &mut Context<Self>) {
2068        if let Some(extension_store) = ExtensionStore::try_global(cx) {
2069            let (manifests, extensions_dir) = {
2070                let store = extension_store.read(cx);
2071                let installed = store.installed_extensions();
2072                let manifests: Vec<_> = installed
2073                    .iter()
2074                    .map(|(id, entry)| (id.clone(), entry.manifest.clone()))
2075                    .collect();
2076                let extensions_dir = paths::extensions_dir().join("installed");
2077                (manifests, extensions_dir)
2078            };
2079
2080            self.project.update(cx, |project, cx| {
2081                project.agent_server_store().update(cx, |store, cx| {
2082                    let manifest_refs: Vec<_> = manifests
2083                        .iter()
2084                        .map(|(id, manifest)| (id.as_ref(), manifest.as_ref()))
2085                        .collect();
2086                    store.sync_extension_agents(manifest_refs, extensions_dir, cx);
2087                });
2088            });
2089        }
2090    }
2091
2092    pub fn new_agent_thread_with_external_source_prompt(
2093        &mut self,
2094        external_source_prompt: Option<ExternalSourcePrompt>,
2095        window: &mut Window,
2096        cx: &mut Context<Self>,
2097    ) {
2098        self.external_thread(
2099            None,
2100            None,
2101            None,
2102            None,
2103            external_source_prompt.map(AgentInitialContent::from),
2104            true,
2105            window,
2106            cx,
2107        );
2108    }
2109
2110    pub fn new_agent_thread(
2111        &mut self,
2112        agent: AgentType,
2113        window: &mut Window,
2114        cx: &mut Context<Self>,
2115    ) {
2116        self.new_agent_thread_inner(agent, true, window, cx);
2117    }
2118
2119    fn new_agent_thread_inner(
2120        &mut self,
2121        agent: AgentType,
2122        focus: bool,
2123        window: &mut Window,
2124        cx: &mut Context<Self>,
2125    ) {
2126        match agent {
2127            AgentType::TextThread => {
2128                window.dispatch_action(NewTextThread.boxed_clone(), cx);
2129            }
2130            AgentType::NativeAgent => self.external_thread(
2131                Some(crate::ExternalAgent::NativeAgent),
2132                None,
2133                None,
2134                None,
2135                None,
2136                focus,
2137                window,
2138                cx,
2139            ),
2140            AgentType::Custom { name } => self.external_thread(
2141                Some(crate::ExternalAgent::Custom { name }),
2142                None,
2143                None,
2144                None,
2145                None,
2146                focus,
2147                window,
2148                cx,
2149            ),
2150        }
2151    }
2152
2153    pub fn load_agent_thread(
2154        &mut self,
2155        session_id: acp::SessionId,
2156        cwd: Option<PathBuf>,
2157        title: Option<SharedString>,
2158        window: &mut Window,
2159        cx: &mut Context<Self>,
2160    ) {
2161        self.load_agent_thread_inner(session_id, cwd, title, true, window, cx);
2162    }
2163
2164    fn load_agent_thread_inner(
2165        &mut self,
2166        session_id: acp::SessionId,
2167        cwd: Option<PathBuf>,
2168        title: Option<SharedString>,
2169        focus: bool,
2170        window: &mut Window,
2171        cx: &mut Context<Self>,
2172    ) {
2173        if let Some(server_view) = self.background_threads.remove(&session_id) {
2174            self.set_active_view(ActiveView::AgentThread { server_view }, focus, window, cx);
2175            return;
2176        }
2177
2178        if let ActiveView::AgentThread { server_view } = &self.active_view {
2179            if server_view
2180                .read(cx)
2181                .active_thread()
2182                .map(|t| t.read(cx).id.clone())
2183                == Some(session_id.clone())
2184            {
2185                cx.emit(AgentPanelEvent::ActiveViewChanged);
2186                return;
2187            }
2188        }
2189
2190        if let Some(ActiveView::AgentThread { server_view }) = &self.previous_view {
2191            if server_view
2192                .read(cx)
2193                .active_thread()
2194                .map(|t| t.read(cx).id.clone())
2195                == Some(session_id.clone())
2196            {
2197                let view = self.previous_view.take().unwrap();
2198                self.set_active_view(view, focus, window, cx);
2199                return;
2200            }
2201        }
2202
2203        let Some(agent) = self.selected_external_agent() else {
2204            return;
2205        };
2206        self.external_thread(
2207            Some(agent),
2208            Some(session_id),
2209            cwd,
2210            title,
2211            None,
2212            focus,
2213            window,
2214            cx,
2215        );
2216    }
2217
2218    pub(crate) fn create_external_thread(
2219        &mut self,
2220        server: Rc<dyn AgentServer>,
2221        resume_session_id: Option<acp::SessionId>,
2222        cwd: Option<PathBuf>,
2223        title: Option<SharedString>,
2224        initial_content: Option<AgentInitialContent>,
2225        workspace: WeakEntity<Workspace>,
2226        project: Entity<Project>,
2227        ext_agent: ExternalAgent,
2228        focus: bool,
2229        window: &mut Window,
2230        cx: &mut Context<Self>,
2231    ) {
2232        let selected_agent = AgentType::from(ext_agent);
2233        if self.selected_agent != selected_agent {
2234            self.selected_agent = selected_agent;
2235            self.serialize(cx);
2236        }
2237        let thread_store = server
2238            .clone()
2239            .downcast::<agent::NativeAgentServer>()
2240            .is_some()
2241            .then(|| self.thread_store.clone());
2242
2243        let server_view = cx.new(|cx| {
2244            crate::ConnectionView::new(
2245                server,
2246                resume_session_id,
2247                cwd,
2248                title,
2249                initial_content,
2250                workspace.clone(),
2251                project,
2252                thread_store,
2253                self.prompt_store.clone(),
2254                self.acp_history.clone(),
2255                window,
2256                cx,
2257            )
2258        });
2259
2260        cx.observe(&server_view, |this, server_view, cx| {
2261            let is_active = this
2262                .as_active_server_view()
2263                .is_some_and(|active| active.entity_id() == server_view.entity_id());
2264            if is_active {
2265                cx.emit(AgentPanelEvent::ActiveViewChanged);
2266                this.serialize(cx);
2267            } else {
2268                cx.emit(AgentPanelEvent::BackgroundThreadChanged);
2269            }
2270            cx.notify();
2271        })
2272        .detach();
2273
2274        self.set_active_view(ActiveView::AgentThread { server_view }, focus, window, cx);
2275    }
2276
2277    fn active_thread_has_messages(&self, cx: &App) -> bool {
2278        self.active_agent_thread(cx)
2279            .is_some_and(|thread| !thread.read(cx).entries().is_empty())
2280    }
2281
2282    fn handle_first_send_requested(
2283        &mut self,
2284        thread_view: Entity<ThreadView>,
2285        content: Vec<acp::ContentBlock>,
2286        window: &mut Window,
2287        cx: &mut Context<Self>,
2288    ) {
2289        if self.start_thread_in == StartThreadIn::NewWorktree {
2290            self.handle_worktree_creation_requested(content, window, cx);
2291        } else {
2292            cx.defer_in(window, move |_this, window, cx| {
2293                thread_view.update(cx, |thread_view, cx| {
2294                    let editor = thread_view.message_editor.clone();
2295                    thread_view.send_impl(editor, window, cx);
2296                });
2297            });
2298        }
2299    }
2300
2301    /// Partitions the project's visible worktrees into git-backed repositories
2302    /// and plain (non-git) paths. Git repos will have worktrees created for
2303    /// them; non-git paths are carried over to the new workspace as-is.
2304    ///
2305    /// When multiple worktrees map to the same repository, the most specific
2306    /// match wins (deepest work directory path), with a deterministic
2307    /// tie-break on entity id. Each repository appears at most once.
2308    fn classify_worktrees(
2309        &self,
2310        cx: &App,
2311    ) -> (Vec<Entity<project::git_store::Repository>>, Vec<PathBuf>) {
2312        let project = &self.project;
2313        let repositories = project.read(cx).repositories(cx).clone();
2314        let mut git_repos: Vec<Entity<project::git_store::Repository>> = Vec::new();
2315        let mut non_git_paths: Vec<PathBuf> = Vec::new();
2316        let mut seen_repo_ids = std::collections::HashSet::new();
2317
2318        for worktree in project.read(cx).visible_worktrees(cx) {
2319            let wt_path = worktree.read(cx).abs_path();
2320
2321            let matching_repo = repositories
2322                .iter()
2323                .filter_map(|(id, repo)| {
2324                    let work_dir = repo.read(cx).work_directory_abs_path.clone();
2325                    if wt_path.starts_with(work_dir.as_ref())
2326                        || work_dir.starts_with(wt_path.as_ref())
2327                    {
2328                        Some((*id, repo.clone(), work_dir.as_ref().components().count()))
2329                    } else {
2330                        None
2331                    }
2332                })
2333                .max_by(
2334                    |(left_id, _left_repo, left_depth), (right_id, _right_repo, right_depth)| {
2335                        left_depth
2336                            .cmp(right_depth)
2337                            .then_with(|| left_id.cmp(right_id))
2338                    },
2339                );
2340
2341            if let Some((id, repo, _)) = matching_repo {
2342                if seen_repo_ids.insert(id) {
2343                    git_repos.push(repo);
2344                }
2345            } else {
2346                non_git_paths.push(wt_path.to_path_buf());
2347            }
2348        }
2349
2350        (git_repos, non_git_paths)
2351    }
2352
2353    /// Kicks off an async git-worktree creation for each repository. Returns:
2354    ///
2355    /// - `creation_infos`: a vec of `(repo, new_path, receiver)` tuples—the
2356    ///   receiver resolves once the git worktree command finishes.
2357    /// - `path_remapping`: `(old_work_dir, new_worktree_path)` pairs used
2358    ///   later to remap open editor tabs into the new workspace.
2359    fn start_worktree_creations(
2360        git_repos: &[Entity<project::git_store::Repository>],
2361        branch_name: &str,
2362        worktree_directory_setting: &str,
2363        cx: &mut Context<Self>,
2364    ) -> Result<(
2365        Vec<(
2366            Entity<project::git_store::Repository>,
2367            PathBuf,
2368            futures::channel::oneshot::Receiver<Result<()>>,
2369        )>,
2370        Vec<(PathBuf, PathBuf)>,
2371    )> {
2372        let mut creation_infos = Vec::new();
2373        let mut path_remapping = Vec::new();
2374
2375        for repo in git_repos {
2376            let (work_dir, new_path, receiver) = repo.update(cx, |repo, _cx| {
2377                let original_repo = repo.original_repo_abs_path.clone();
2378                let directory =
2379                    validate_worktree_directory(&original_repo, worktree_directory_setting)?;
2380                let new_path = directory.join(branch_name);
2381                let receiver = repo.create_worktree(branch_name.to_string(), directory, None);
2382                let work_dir = repo.work_directory_abs_path.clone();
2383                anyhow::Ok((work_dir, new_path, receiver))
2384            })?;
2385            path_remapping.push((work_dir.to_path_buf(), new_path.clone()));
2386            creation_infos.push((repo.clone(), new_path, receiver));
2387        }
2388
2389        Ok((creation_infos, path_remapping))
2390    }
2391
2392    /// Waits for every in-flight worktree creation to complete. If any
2393    /// creation fails, all successfully-created worktrees are rolled back
2394    /// (removed) so the project isn't left in a half-migrated state.
2395    async fn await_and_rollback_on_failure(
2396        creation_infos: Vec<(
2397            Entity<project::git_store::Repository>,
2398            PathBuf,
2399            futures::channel::oneshot::Receiver<Result<()>>,
2400        )>,
2401        cx: &mut AsyncWindowContext,
2402    ) -> Result<Vec<PathBuf>> {
2403        let mut created_paths: Vec<PathBuf> = Vec::new();
2404        let mut repos_and_paths: Vec<(Entity<project::git_store::Repository>, PathBuf)> =
2405            Vec::new();
2406        let mut first_error: Option<anyhow::Error> = None;
2407
2408        for (repo, new_path, receiver) in creation_infos {
2409            match receiver.await {
2410                Ok(Ok(())) => {
2411                    created_paths.push(new_path.clone());
2412                    repos_and_paths.push((repo, new_path));
2413                }
2414                Ok(Err(err)) => {
2415                    if first_error.is_none() {
2416                        first_error = Some(err);
2417                    }
2418                }
2419                Err(_canceled) => {
2420                    if first_error.is_none() {
2421                        first_error = Some(anyhow!("Worktree creation was canceled"));
2422                    }
2423                }
2424            }
2425        }
2426
2427        let Some(err) = first_error else {
2428            return Ok(created_paths);
2429        };
2430
2431        // Rollback all successfully created worktrees
2432        let mut rollback_receivers = Vec::new();
2433        for (rollback_repo, rollback_path) in &repos_and_paths {
2434            if let Ok(receiver) = cx.update(|_, cx| {
2435                rollback_repo.update(cx, |repo, _cx| {
2436                    repo.remove_worktree(rollback_path.clone(), true)
2437                })
2438            }) {
2439                rollback_receivers.push((rollback_path.clone(), receiver));
2440            }
2441        }
2442        let mut rollback_failures: Vec<String> = Vec::new();
2443        for (path, receiver) in rollback_receivers {
2444            match receiver.await {
2445                Ok(Ok(())) => {}
2446                Ok(Err(rollback_err)) => {
2447                    log::error!(
2448                        "failed to rollback worktree at {}: {rollback_err}",
2449                        path.display()
2450                    );
2451                    rollback_failures.push(format!("{}: {rollback_err}", path.display()));
2452                }
2453                Err(rollback_err) => {
2454                    log::error!(
2455                        "failed to rollback worktree at {}: {rollback_err}",
2456                        path.display()
2457                    );
2458                    rollback_failures.push(format!("{}: {rollback_err}", path.display()));
2459                }
2460            }
2461        }
2462        let mut error_message = format!("Failed to create worktree: {err}");
2463        if !rollback_failures.is_empty() {
2464            error_message.push_str("\n\nFailed to clean up: ");
2465            error_message.push_str(&rollback_failures.join(", "));
2466        }
2467        Err(anyhow!(error_message))
2468    }
2469
2470    fn set_worktree_creation_error(
2471        &mut self,
2472        message: SharedString,
2473        window: &mut Window,
2474        cx: &mut Context<Self>,
2475    ) {
2476        self.worktree_creation_status = Some(WorktreeCreationStatus::Error(message));
2477        if matches!(self.active_view, ActiveView::Uninitialized) {
2478            let selected_agent = self.selected_agent.clone();
2479            self.new_agent_thread(selected_agent, window, cx);
2480        }
2481        cx.notify();
2482    }
2483
2484    fn handle_worktree_creation_requested(
2485        &mut self,
2486        content: Vec<acp::ContentBlock>,
2487        window: &mut Window,
2488        cx: &mut Context<Self>,
2489    ) {
2490        if matches!(
2491            self.worktree_creation_status,
2492            Some(WorktreeCreationStatus::Creating)
2493        ) {
2494            return;
2495        }
2496
2497        self.worktree_creation_status = Some(WorktreeCreationStatus::Creating);
2498        cx.notify();
2499
2500        let (git_repos, non_git_paths) = self.classify_worktrees(cx);
2501
2502        if git_repos.is_empty() {
2503            self.set_worktree_creation_error(
2504                "No git repositories found in the project".into(),
2505                window,
2506                cx,
2507            );
2508            return;
2509        }
2510
2511        // Kick off branch listing as early as possible so it can run
2512        // concurrently with the remaining synchronous setup work.
2513        let branch_receivers: Vec<_> = git_repos
2514            .iter()
2515            .map(|repo| repo.update(cx, |repo, _cx| repo.branches()))
2516            .collect();
2517
2518        let worktree_directory_setting = ProjectSettings::get_global(cx)
2519            .git
2520            .worktree_directory
2521            .clone();
2522
2523        let (dock_structure, open_file_paths) = self
2524            .workspace
2525            .upgrade()
2526            .map(|workspace| {
2527                let dock_structure = workspace.read(cx).capture_dock_state(window, cx);
2528                let open_file_paths = workspace.read(cx).open_item_abs_paths(cx);
2529                (dock_structure, open_file_paths)
2530            })
2531            .unwrap_or_default();
2532
2533        let workspace = self.workspace.clone();
2534        let window_handle = window
2535            .window_handle()
2536            .downcast::<workspace::MultiWorkspace>();
2537
2538        let task = cx.spawn_in(window, async move |this, cx| {
2539            // Await the branch listings we kicked off earlier.
2540            let mut existing_branches = Vec::new();
2541            for result in futures::future::join_all(branch_receivers).await {
2542                match result {
2543                    Ok(Ok(branches)) => {
2544                        for branch in branches {
2545                            existing_branches.push(branch.name().to_string());
2546                        }
2547                    }
2548                    Ok(Err(err)) => {
2549                        Err::<(), _>(err).log_err();
2550                    }
2551                    Err(_) => {}
2552                }
2553            }
2554
2555            let existing_branch_refs: Vec<&str> =
2556                existing_branches.iter().map(|s| s.as_str()).collect();
2557            let mut rng = rand::rng();
2558            let branch_name =
2559                match crate::branch_names::generate_branch_name(&existing_branch_refs, &mut rng) {
2560                    Some(name) => name,
2561                    None => {
2562                        this.update_in(cx, |this, window, cx| {
2563                            this.set_worktree_creation_error(
2564                                "Failed to generate a branch name: all typewriter names are taken"
2565                                    .into(),
2566                                window,
2567                                cx,
2568                            );
2569                        })?;
2570                        return anyhow::Ok(());
2571                    }
2572                };
2573
2574            let (creation_infos, path_remapping) = match this.update_in(cx, |_this, _window, cx| {
2575                Self::start_worktree_creations(
2576                    &git_repos,
2577                    &branch_name,
2578                    &worktree_directory_setting,
2579                    cx,
2580                )
2581            }) {
2582                Ok(Ok(result)) => result,
2583                Ok(Err(err)) | Err(err) => {
2584                    this.update_in(cx, |this, window, cx| {
2585                        this.set_worktree_creation_error(
2586                            format!("Failed to validate worktree directory: {err}").into(),
2587                            window,
2588                            cx,
2589                        );
2590                    })
2591                    .log_err();
2592                    return anyhow::Ok(());
2593                }
2594            };
2595
2596            let created_paths = match Self::await_and_rollback_on_failure(creation_infos, cx).await
2597            {
2598                Ok(paths) => paths,
2599                Err(err) => {
2600                    this.update_in(cx, |this, window, cx| {
2601                        this.set_worktree_creation_error(format!("{err}").into(), window, cx);
2602                    })?;
2603                    return anyhow::Ok(());
2604                }
2605            };
2606
2607            let mut all_paths = created_paths;
2608            let has_non_git = !non_git_paths.is_empty();
2609            all_paths.extend(non_git_paths.iter().cloned());
2610
2611            let app_state = match workspace.upgrade() {
2612                Some(workspace) => cx.update(|_, cx| workspace.read(cx).app_state().clone())?,
2613                None => {
2614                    this.update_in(cx, |this, window, cx| {
2615                        this.set_worktree_creation_error(
2616                            "Workspace no longer available".into(),
2617                            window,
2618                            cx,
2619                        );
2620                    })?;
2621                    return anyhow::Ok(());
2622                }
2623            };
2624
2625            let this_for_error = this.clone();
2626            if let Err(err) = Self::setup_new_workspace(
2627                this,
2628                all_paths,
2629                app_state,
2630                window_handle,
2631                dock_structure,
2632                open_file_paths,
2633                path_remapping,
2634                non_git_paths,
2635                has_non_git,
2636                content,
2637                cx,
2638            )
2639            .await
2640            {
2641                this_for_error
2642                    .update_in(cx, |this, window, cx| {
2643                        this.set_worktree_creation_error(
2644                            format!("Failed to set up workspace: {err}").into(),
2645                            window,
2646                            cx,
2647                        );
2648                    })
2649                    .log_err();
2650            }
2651            anyhow::Ok(())
2652        });
2653
2654        self._worktree_creation_task = Some(cx.foreground_executor().spawn(async move {
2655            task.await.log_err();
2656        }));
2657    }
2658
2659    async fn setup_new_workspace(
2660        this: WeakEntity<Self>,
2661        all_paths: Vec<PathBuf>,
2662        app_state: Arc<workspace::AppState>,
2663        window_handle: Option<gpui::WindowHandle<workspace::MultiWorkspace>>,
2664        dock_structure: workspace::DockStructure,
2665        open_file_paths: Vec<PathBuf>,
2666        path_remapping: Vec<(PathBuf, PathBuf)>,
2667        non_git_paths: Vec<PathBuf>,
2668        has_non_git: bool,
2669        content: Vec<acp::ContentBlock>,
2670        cx: &mut AsyncWindowContext,
2671    ) -> Result<()> {
2672        let init: Option<
2673            Box<dyn FnOnce(&mut Workspace, &mut Window, &mut gpui::Context<Workspace>) + Send>,
2674        > = Some(Box::new(move |workspace, window, cx| {
2675            workspace.set_dock_structure(dock_structure, window, cx);
2676        }));
2677
2678        let (new_window_handle, _) = cx
2679            .update(|_window, cx| {
2680                Workspace::new_local(all_paths, app_state, window_handle, None, init, false, cx)
2681            })?
2682            .await?;
2683
2684        let new_workspace = new_window_handle.update(cx, |multi_workspace, _window, _cx| {
2685            let workspaces = multi_workspace.workspaces();
2686            workspaces.last().cloned()
2687        })?;
2688
2689        let Some(new_workspace) = new_workspace else {
2690            anyhow::bail!("New workspace was not added to MultiWorkspace");
2691        };
2692
2693        let panels_task = new_window_handle.update(cx, |_, _, cx| {
2694            new_workspace.update(cx, |workspace, _cx| workspace.take_panels_task())
2695        })?;
2696        if let Some(task) = panels_task {
2697            task.await.log_err();
2698        }
2699
2700        let initial_content = AgentInitialContent::ContentBlock {
2701            blocks: content,
2702            auto_submit: true,
2703        };
2704
2705        new_window_handle.update(cx, |_multi_workspace, window, cx| {
2706            new_workspace.update(cx, |workspace, cx| {
2707                if has_non_git {
2708                    let toast_id = workspace::notifications::NotificationId::unique::<AgentPanel>();
2709                    workspace.show_toast(
2710                        workspace::Toast::new(
2711                            toast_id,
2712                            "Some project folders are not git repositories. \
2713                             They were included as-is without creating a worktree.",
2714                        ),
2715                        cx,
2716                    );
2717                }
2718
2719                let remapped_paths: Vec<PathBuf> = open_file_paths
2720                    .iter()
2721                    .filter_map(|original_path| {
2722                        let best_match = path_remapping
2723                            .iter()
2724                            .filter_map(|(old_root, new_root)| {
2725                                original_path.strip_prefix(old_root).ok().map(|relative| {
2726                                    (old_root.components().count(), new_root.join(relative))
2727                                })
2728                            })
2729                            .max_by_key(|(depth, _)| *depth);
2730
2731                        if let Some((_, remapped_path)) = best_match {
2732                            return Some(remapped_path);
2733                        }
2734
2735                        for non_git in &non_git_paths {
2736                            if original_path.starts_with(non_git) {
2737                                return Some(original_path.clone());
2738                            }
2739                        }
2740                        None
2741                    })
2742                    .collect();
2743
2744                if !remapped_paths.is_empty() {
2745                    workspace
2746                        .open_paths(
2747                            remapped_paths,
2748                            workspace::OpenOptions::default(),
2749                            None,
2750                            window,
2751                            cx,
2752                        )
2753                        .detach();
2754                }
2755
2756                workspace.focus_panel::<AgentPanel>(window, cx);
2757                if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
2758                    panel.update(cx, |panel, cx| {
2759                        panel.external_thread(
2760                            None,
2761                            None,
2762                            None,
2763                            None,
2764                            Some(initial_content),
2765                            true,
2766                            window,
2767                            cx,
2768                        );
2769                    });
2770                }
2771            });
2772        })?;
2773
2774        new_window_handle.update(cx, |multi_workspace, _window, cx| {
2775            multi_workspace.activate(new_workspace.clone(), cx);
2776        })?;
2777
2778        this.update_in(cx, |this, _window, cx| {
2779            this.worktree_creation_status = None;
2780            cx.notify();
2781        })?;
2782
2783        anyhow::Ok(())
2784    }
2785}
2786
2787impl Focusable for AgentPanel {
2788    fn focus_handle(&self, cx: &App) -> FocusHandle {
2789        match &self.active_view {
2790            ActiveView::Uninitialized => self.focus_handle.clone(),
2791            ActiveView::AgentThread { server_view, .. } => server_view.focus_handle(cx),
2792            ActiveView::History { kind } => match kind {
2793                HistoryKind::AgentThreads => self.acp_history.focus_handle(cx),
2794                HistoryKind::TextThreads => self.text_thread_history.focus_handle(cx),
2795            },
2796            ActiveView::TextThread {
2797                text_thread_editor, ..
2798            } => text_thread_editor.focus_handle(cx),
2799            ActiveView::Configuration => {
2800                if let Some(configuration) = self.configuration.as_ref() {
2801                    configuration.focus_handle(cx)
2802                } else {
2803                    self.focus_handle.clone()
2804                }
2805            }
2806        }
2807    }
2808}
2809
2810fn agent_panel_dock_position(cx: &App) -> DockPosition {
2811    AgentSettings::get_global(cx).dock.into()
2812}
2813
2814pub enum AgentPanelEvent {
2815    ActiveViewChanged,
2816    ThreadFocused,
2817    BackgroundThreadChanged,
2818}
2819
2820impl EventEmitter<PanelEvent> for AgentPanel {}
2821impl EventEmitter<AgentPanelEvent> for AgentPanel {}
2822
2823impl Panel for AgentPanel {
2824    fn persistent_name() -> &'static str {
2825        "AgentPanel"
2826    }
2827
2828    fn panel_key() -> &'static str {
2829        AGENT_PANEL_KEY
2830    }
2831
2832    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
2833        agent_panel_dock_position(cx)
2834    }
2835
2836    fn position_is_valid(&self, position: DockPosition) -> bool {
2837        position != DockPosition::Bottom
2838    }
2839
2840    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
2841        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
2842            settings
2843                .agent
2844                .get_or_insert_default()
2845                .set_dock(position.into());
2846        });
2847    }
2848
2849    fn size(&self, window: &Window, cx: &App) -> Pixels {
2850        let settings = AgentSettings::get_global(cx);
2851        match self.position(window, cx) {
2852            DockPosition::Left | DockPosition::Right => {
2853                self.width.unwrap_or(settings.default_width)
2854            }
2855            DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
2856        }
2857    }
2858
2859    fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
2860        match self.position(window, cx) {
2861            DockPosition::Left | DockPosition::Right => self.width = size,
2862            DockPosition::Bottom => self.height = size,
2863        }
2864        self.serialize(cx);
2865        cx.notify();
2866    }
2867
2868    fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
2869        if active
2870            && matches!(self.active_view, ActiveView::Uninitialized)
2871            && !matches!(
2872                self.worktree_creation_status,
2873                Some(WorktreeCreationStatus::Creating)
2874            )
2875        {
2876            let selected_agent = self.selected_agent.clone();
2877            self.new_agent_thread_inner(selected_agent, false, window, cx);
2878        }
2879    }
2880
2881    fn remote_id() -> Option<proto::PanelId> {
2882        Some(proto::PanelId::AssistantPanel)
2883    }
2884
2885    fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
2886        (self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAssistant)
2887    }
2888
2889    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
2890        Some("Agent Panel")
2891    }
2892
2893    fn toggle_action(&self) -> Box<dyn Action> {
2894        Box::new(ToggleFocus)
2895    }
2896
2897    fn activation_priority(&self) -> u32 {
2898        3
2899    }
2900
2901    fn enabled(&self, cx: &App) -> bool {
2902        AgentSettings::get_global(cx).enabled(cx)
2903    }
2904
2905    fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
2906        self.zoomed
2907    }
2908
2909    fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
2910        self.zoomed = zoomed;
2911        cx.notify();
2912    }
2913}
2914
2915impl AgentPanel {
2916    fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
2917        const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
2918
2919        let content = match &self.active_view {
2920            ActiveView::AgentThread { server_view } => {
2921                let is_generating_title = server_view
2922                    .read(cx)
2923                    .as_native_thread(cx)
2924                    .map_or(false, |t| t.read(cx).is_generating_title());
2925
2926                if let Some(title_editor) = server_view
2927                    .read(cx)
2928                    .parent_thread(cx)
2929                    .map(|r| r.read(cx).title_editor.clone())
2930                {
2931                    let container = div()
2932                        .w_full()
2933                        .on_action({
2934                            let thread_view = server_view.downgrade();
2935                            move |_: &menu::Confirm, window, cx| {
2936                                if let Some(thread_view) = thread_view.upgrade() {
2937                                    thread_view.focus_handle(cx).focus(window, cx);
2938                                }
2939                            }
2940                        })
2941                        .on_action({
2942                            let thread_view = server_view.downgrade();
2943                            move |_: &editor::actions::Cancel, window, cx| {
2944                                if let Some(thread_view) = thread_view.upgrade() {
2945                                    thread_view.focus_handle(cx).focus(window, cx);
2946                                }
2947                            }
2948                        })
2949                        .child(title_editor);
2950
2951                    if is_generating_title {
2952                        container
2953                            .with_animation(
2954                                "generating_title",
2955                                Animation::new(Duration::from_secs(2))
2956                                    .repeat()
2957                                    .with_easing(pulsating_between(0.4, 0.8)),
2958                                |div, delta| div.opacity(delta),
2959                            )
2960                            .into_any_element()
2961                    } else {
2962                        container.into_any_element()
2963                    }
2964                } else {
2965                    Label::new(server_view.read(cx).title(cx))
2966                        .color(Color::Muted)
2967                        .truncate()
2968                        .into_any_element()
2969                }
2970            }
2971            ActiveView::TextThread {
2972                title_editor,
2973                text_thread_editor,
2974                ..
2975            } => {
2976                let summary = text_thread_editor.read(cx).text_thread().read(cx).summary();
2977
2978                match summary {
2979                    TextThreadSummary::Pending => Label::new(TextThreadSummary::DEFAULT)
2980                        .color(Color::Muted)
2981                        .truncate()
2982                        .into_any_element(),
2983                    TextThreadSummary::Content(summary) => {
2984                        if summary.done {
2985                            div()
2986                                .w_full()
2987                                .child(title_editor.clone())
2988                                .into_any_element()
2989                        } else {
2990                            Label::new(LOADING_SUMMARY_PLACEHOLDER)
2991                                .truncate()
2992                                .color(Color::Muted)
2993                                .with_animation(
2994                                    "generating_title",
2995                                    Animation::new(Duration::from_secs(2))
2996                                        .repeat()
2997                                        .with_easing(pulsating_between(0.4, 0.8)),
2998                                    |label, delta| label.alpha(delta),
2999                                )
3000                                .into_any_element()
3001                        }
3002                    }
3003                    TextThreadSummary::Error => h_flex()
3004                        .w_full()
3005                        .child(title_editor.clone())
3006                        .child(
3007                            IconButton::new("retry-summary-generation", IconName::RotateCcw)
3008                                .icon_size(IconSize::Small)
3009                                .on_click({
3010                                    let text_thread_editor = text_thread_editor.clone();
3011                                    move |_, _window, cx| {
3012                                        text_thread_editor.update(cx, |text_thread_editor, cx| {
3013                                            text_thread_editor.regenerate_summary(cx);
3014                                        });
3015                                    }
3016                                })
3017                                .tooltip(move |_window, cx| {
3018                                    cx.new(|_| {
3019                                        Tooltip::new("Failed to generate title")
3020                                            .meta("Click to try again")
3021                                    })
3022                                    .into()
3023                                }),
3024                        )
3025                        .into_any_element(),
3026                }
3027            }
3028            ActiveView::History { kind } => {
3029                let title = match kind {
3030                    HistoryKind::AgentThreads => "History",
3031                    HistoryKind::TextThreads => "Text Thread History",
3032                };
3033                Label::new(title).truncate().into_any_element()
3034            }
3035            ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
3036            ActiveView::Uninitialized => Label::new("Agent").truncate().into_any_element(),
3037        };
3038
3039        h_flex()
3040            .key_context("TitleEditor")
3041            .id("TitleEditor")
3042            .flex_grow()
3043            .w_full()
3044            .max_w_full()
3045            .overflow_x_scroll()
3046            .child(content)
3047            .into_any()
3048    }
3049
3050    fn handle_regenerate_thread_title(thread_view: Entity<ConnectionView>, cx: &mut App) {
3051        thread_view.update(cx, |thread_view, cx| {
3052            if let Some(thread) = thread_view.as_native_thread(cx) {
3053                thread.update(cx, |thread, cx| {
3054                    thread.generate_title(cx);
3055                });
3056            }
3057        });
3058    }
3059
3060    fn handle_regenerate_text_thread_title(
3061        text_thread_editor: Entity<TextThreadEditor>,
3062        cx: &mut App,
3063    ) {
3064        text_thread_editor.update(cx, |text_thread_editor, cx| {
3065            text_thread_editor.regenerate_summary(cx);
3066        });
3067    }
3068
3069    fn render_panel_options_menu(
3070        &self,
3071        window: &mut Window,
3072        cx: &mut Context<Self>,
3073    ) -> impl IntoElement {
3074        let focus_handle = self.focus_handle(cx);
3075
3076        let full_screen_label = if self.is_zoomed(window, cx) {
3077            "Disable Full Screen"
3078        } else {
3079            "Enable Full Screen"
3080        };
3081
3082        let text_thread_view = match &self.active_view {
3083            ActiveView::TextThread {
3084                text_thread_editor, ..
3085            } => Some(text_thread_editor.clone()),
3086            _ => None,
3087        };
3088        let text_thread_with_messages = match &self.active_view {
3089            ActiveView::TextThread {
3090                text_thread_editor, ..
3091            } => text_thread_editor
3092                .read(cx)
3093                .text_thread()
3094                .read(cx)
3095                .messages(cx)
3096                .any(|message| message.role == language_model::Role::Assistant),
3097            _ => false,
3098        };
3099
3100        let thread_view = match &self.active_view {
3101            ActiveView::AgentThread { server_view } => Some(server_view.clone()),
3102            _ => None,
3103        };
3104        let thread_with_messages = match &self.active_view {
3105            ActiveView::AgentThread { server_view } => {
3106                server_view.read(cx).has_user_submitted_prompt(cx)
3107            }
3108            _ => false,
3109        };
3110        let has_auth_methods = match &self.active_view {
3111            ActiveView::AgentThread { server_view } => server_view.read(cx).has_auth_methods(),
3112            _ => false,
3113        };
3114
3115        PopoverMenu::new("agent-options-menu")
3116            .trigger_with_tooltip(
3117                IconButton::new("agent-options-menu", IconName::Ellipsis)
3118                    .icon_size(IconSize::Small),
3119                {
3120                    let focus_handle = focus_handle.clone();
3121                    move |_window, cx| {
3122                        Tooltip::for_action_in(
3123                            "Toggle Agent Menu",
3124                            &ToggleOptionsMenu,
3125                            &focus_handle,
3126                            cx,
3127                        )
3128                    }
3129                },
3130            )
3131            .anchor(Corner::TopRight)
3132            .with_handle(self.agent_panel_menu_handle.clone())
3133            .menu({
3134                move |window, cx| {
3135                    Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
3136                        menu = menu.context(focus_handle.clone());
3137
3138                        if thread_with_messages | text_thread_with_messages {
3139                            menu = menu.header("Current Thread");
3140
3141                            if let Some(text_thread_view) = text_thread_view.as_ref() {
3142                                menu = menu
3143                                    .entry("Regenerate Thread Title", None, {
3144                                        let text_thread_view = text_thread_view.clone();
3145                                        move |_, cx| {
3146                                            Self::handle_regenerate_text_thread_title(
3147                                                text_thread_view.clone(),
3148                                                cx,
3149                                            );
3150                                        }
3151                                    })
3152                                    .separator();
3153                            }
3154
3155                            if let Some(thread_view) = thread_view.as_ref() {
3156                                menu = menu
3157                                    .entry("Regenerate Thread Title", None, {
3158                                        let thread_view = thread_view.clone();
3159                                        move |_, cx| {
3160                                            Self::handle_regenerate_thread_title(
3161                                                thread_view.clone(),
3162                                                cx,
3163                                            );
3164                                        }
3165                                    })
3166                                    .separator();
3167                            }
3168                        }
3169
3170                        menu = menu
3171                            .header("MCP Servers")
3172                            .action(
3173                                "View Server Extensions",
3174                                Box::new(zed_actions::Extensions {
3175                                    category_filter: Some(
3176                                        zed_actions::ExtensionCategoryFilter::ContextServers,
3177                                    ),
3178                                    id: None,
3179                                }),
3180                            )
3181                            .action("Add Custom Server…", Box::new(AddContextServer))
3182                            .separator()
3183                            .action("Rules", Box::new(OpenRulesLibrary::default()))
3184                            .action("Profiles", Box::new(ManageProfiles::default()))
3185                            .action("Settings", Box::new(OpenSettings))
3186                            .separator()
3187                            .action(full_screen_label, Box::new(ToggleZoom));
3188
3189                        if has_auth_methods {
3190                            menu = menu.action("Reauthenticate", Box::new(ReauthenticateAgent))
3191                        }
3192
3193                        menu
3194                    }))
3195                }
3196            })
3197    }
3198
3199    fn render_recent_entries_menu(
3200        &self,
3201        icon: IconName,
3202        corner: Corner,
3203        cx: &mut Context<Self>,
3204    ) -> impl IntoElement {
3205        let focus_handle = self.focus_handle(cx);
3206
3207        PopoverMenu::new("agent-nav-menu")
3208            .trigger_with_tooltip(
3209                IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small),
3210                {
3211                    move |_window, cx| {
3212                        Tooltip::for_action_in(
3213                            "Toggle Recently Updated Threads",
3214                            &ToggleNavigationMenu,
3215                            &focus_handle,
3216                            cx,
3217                        )
3218                    }
3219                },
3220            )
3221            .anchor(corner)
3222            .with_handle(self.agent_navigation_menu_handle.clone())
3223            .menu({
3224                let menu = self.agent_navigation_menu.clone();
3225                move |window, cx| {
3226                    telemetry::event!("View Thread History Clicked");
3227
3228                    if let Some(menu) = menu.as_ref() {
3229                        menu.update(cx, |_, cx| {
3230                            cx.defer_in(window, |menu, window, cx| {
3231                                menu.rebuild(window, cx);
3232                            });
3233                        })
3234                    }
3235                    menu.clone()
3236                }
3237            })
3238    }
3239
3240    fn render_toolbar_back_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
3241        let focus_handle = self.focus_handle(cx);
3242
3243        IconButton::new("go-back", IconName::ArrowLeft)
3244            .icon_size(IconSize::Small)
3245            .on_click(cx.listener(|this, _, window, cx| {
3246                this.go_back(&workspace::GoBack, window, cx);
3247            }))
3248            .tooltip({
3249                move |_window, cx| {
3250                    Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, cx)
3251                }
3252            })
3253    }
3254
3255    fn project_has_git_repository(&self, cx: &App) -> bool {
3256        !self.project.read(cx).repositories(cx).is_empty()
3257    }
3258
3259    fn render_start_thread_in_selector(&self, cx: &mut Context<Self>) -> impl IntoElement {
3260        let focus_handle = self.focus_handle(cx);
3261        let has_git_repo = self.project_has_git_repository(cx);
3262        let is_via_collab = self.project.read(cx).is_via_collab();
3263
3264        let is_creating = matches!(
3265            self.worktree_creation_status,
3266            Some(WorktreeCreationStatus::Creating)
3267        );
3268
3269        let current_target = self.start_thread_in;
3270        let trigger_label = self.start_thread_in.label();
3271
3272        let icon = if self.start_thread_in_menu_handle.is_deployed() {
3273            IconName::ChevronUp
3274        } else {
3275            IconName::ChevronDown
3276        };
3277
3278        let trigger_button = Button::new("thread-target-trigger", trigger_label)
3279            .icon(icon)
3280            .icon_size(IconSize::XSmall)
3281            .icon_position(IconPosition::End)
3282            .icon_color(Color::Muted)
3283            .selected_style(ButtonStyle::Tinted(TintColor::Accent))
3284            .disabled(is_creating);
3285
3286        let dock_position = AgentSettings::get_global(cx).dock;
3287        let documentation_side = match dock_position {
3288            settings::DockPosition::Left => DocumentationSide::Right,
3289            settings::DockPosition::Bottom | settings::DockPosition::Right => {
3290                DocumentationSide::Left
3291            }
3292        };
3293
3294        PopoverMenu::new("thread-target-selector")
3295            .trigger_with_tooltip(trigger_button, {
3296                move |_window, cx| {
3297                    Tooltip::for_action_in(
3298                        "Start Thread In…",
3299                        &ToggleStartThreadInSelector,
3300                        &focus_handle,
3301                        cx,
3302                    )
3303                }
3304            })
3305            .menu(move |window, cx| {
3306                let is_local_selected = current_target == StartThreadIn::LocalProject;
3307                let is_new_worktree_selected = current_target == StartThreadIn::NewWorktree;
3308
3309                Some(ContextMenu::build(window, cx, move |menu, _window, _cx| {
3310                    let new_worktree_disabled = !has_git_repo || is_via_collab;
3311
3312                    menu.header("Start Thread In…")
3313                        .item(
3314                            ContextMenuEntry::new("Current Project")
3315                                .toggleable(IconPosition::End, is_local_selected)
3316                                .handler(|window, cx| {
3317                                    window
3318                                        .dispatch_action(Box::new(StartThreadIn::LocalProject), cx);
3319                                }),
3320                        )
3321                        .item({
3322                            let entry = ContextMenuEntry::new("New Worktree")
3323                                .toggleable(IconPosition::End, is_new_worktree_selected)
3324                                .disabled(new_worktree_disabled)
3325                                .handler(|window, cx| {
3326                                    window
3327                                        .dispatch_action(Box::new(StartThreadIn::NewWorktree), cx);
3328                                });
3329
3330                            if new_worktree_disabled {
3331                                entry.documentation_aside(documentation_side, move |_| {
3332                                    let reason = if !has_git_repo {
3333                                        "No git repository found in this project."
3334                                    } else {
3335                                        "Not available for remote/collab projects yet."
3336                                    };
3337                                    Label::new(reason)
3338                                        .color(Color::Muted)
3339                                        .size(LabelSize::Small)
3340                                        .into_any_element()
3341                                })
3342                            } else {
3343                                entry
3344                            }
3345                        })
3346                }))
3347            })
3348            .with_handle(self.start_thread_in_menu_handle.clone())
3349            .anchor(Corner::TopLeft)
3350            .offset(gpui::Point {
3351                x: px(1.0),
3352                y: px(1.0),
3353            })
3354    }
3355
3356    fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3357        let agent_server_store = self.project.read(cx).agent_server_store().clone();
3358        let focus_handle = self.focus_handle(cx);
3359
3360        let (selected_agent_custom_icon, selected_agent_label) =
3361            if let AgentType::Custom { name, .. } = &self.selected_agent {
3362                let store = agent_server_store.read(cx);
3363                let icon = store.agent_icon(&ExternalAgentServerName(name.clone()));
3364
3365                let label = store
3366                    .agent_display_name(&ExternalAgentServerName(name.clone()))
3367                    .unwrap_or_else(|| self.selected_agent.label());
3368                (icon, label)
3369            } else {
3370                (None, self.selected_agent.label())
3371            };
3372
3373        let active_thread = match &self.active_view {
3374            ActiveView::AgentThread { server_view } => server_view.read(cx).as_native_thread(cx),
3375            ActiveView::Uninitialized
3376            | ActiveView::TextThread { .. }
3377            | ActiveView::History { .. }
3378            | ActiveView::Configuration => None,
3379        };
3380
3381        let new_thread_menu_builder: Rc<
3382            dyn Fn(&mut Window, &mut App) -> Option<Entity<ContextMenu>>,
3383        > = {
3384            let selected_agent = self.selected_agent.clone();
3385            let is_agent_selected = move |agent_type: AgentType| selected_agent == agent_type;
3386
3387            let workspace = self.workspace.clone();
3388            let is_via_collab = workspace
3389                .update(cx, |workspace, cx| {
3390                    workspace.project().read(cx).is_via_collab()
3391                })
3392                .unwrap_or_default();
3393
3394            let focus_handle = focus_handle.clone();
3395            let agent_server_store = agent_server_store;
3396
3397            Rc::new(move |window, cx| {
3398                telemetry::event!("New Thread Clicked");
3399
3400                let active_thread = active_thread.clone();
3401                Some(ContextMenu::build(window, cx, |menu, _window, cx| {
3402                    menu.context(focus_handle.clone())
3403                        .when_some(active_thread, |this, active_thread| {
3404                            let thread = active_thread.read(cx);
3405
3406                            if !thread.is_empty() {
3407                                let session_id = thread.id().clone();
3408                                this.item(
3409                                    ContextMenuEntry::new("New From Summary")
3410                                        .icon(IconName::ThreadFromSummary)
3411                                        .icon_color(Color::Muted)
3412                                        .handler(move |window, cx| {
3413                                            window.dispatch_action(
3414                                                Box::new(NewNativeAgentThreadFromSummary {
3415                                                    from_session_id: session_id.clone(),
3416                                                }),
3417                                                cx,
3418                                            );
3419                                        }),
3420                                )
3421                            } else {
3422                                this
3423                            }
3424                        })
3425                        .item(
3426                            ContextMenuEntry::new("Zed Agent")
3427                                .when(
3428                                    is_agent_selected(AgentType::NativeAgent)
3429                                        | is_agent_selected(AgentType::TextThread),
3430                                    |this| {
3431                                        this.action(Box::new(NewExternalAgentThread {
3432                                            agent: None,
3433                                        }))
3434                                    },
3435                                )
3436                                .icon(IconName::ZedAgent)
3437                                .icon_color(Color::Muted)
3438                                .handler({
3439                                    let workspace = workspace.clone();
3440                                    move |window, cx| {
3441                                        if let Some(workspace) = workspace.upgrade() {
3442                                            workspace.update(cx, |workspace, cx| {
3443                                                if let Some(panel) =
3444                                                    workspace.panel::<AgentPanel>(cx)
3445                                                {
3446                                                    panel.update(cx, |panel, cx| {
3447                                                        panel.new_agent_thread(
3448                                                            AgentType::NativeAgent,
3449                                                            window,
3450                                                            cx,
3451                                                        );
3452                                                    });
3453                                                }
3454                                            });
3455                                        }
3456                                    }
3457                                }),
3458                        )
3459                        .item(
3460                            ContextMenuEntry::new("Text Thread")
3461                                .action(NewTextThread.boxed_clone())
3462                                .icon(IconName::TextThread)
3463                                .icon_color(Color::Muted)
3464                                .handler({
3465                                    let workspace = workspace.clone();
3466                                    move |window, cx| {
3467                                        if let Some(workspace) = workspace.upgrade() {
3468                                            workspace.update(cx, |workspace, cx| {
3469                                                if let Some(panel) =
3470                                                    workspace.panel::<AgentPanel>(cx)
3471                                                {
3472                                                    panel.update(cx, |panel, cx| {
3473                                                        panel.new_agent_thread(
3474                                                            AgentType::TextThread,
3475                                                            window,
3476                                                            cx,
3477                                                        );
3478                                                    });
3479                                                }
3480                                            });
3481                                        }
3482                                    }
3483                                }),
3484                        )
3485                        .separator()
3486                        .header("External Agents")
3487                        .map(|mut menu| {
3488                            let agent_server_store = agent_server_store.read(cx);
3489                            let registry_store =
3490                                project::AgentRegistryStore::try_global(cx);
3491                            let registry_store_ref =
3492                                registry_store.as_ref().map(|s| s.read(cx));
3493
3494                            struct AgentMenuItem {
3495                                id: ExternalAgentServerName,
3496                                display_name: SharedString,
3497                            }
3498
3499                            let agent_items = agent_server_store
3500                                .external_agents()
3501                                .map(|name| {
3502                                    let display_name = agent_server_store
3503                                        .agent_display_name(name)
3504                                        .or_else(|| {
3505                                            registry_store_ref
3506                                                .as_ref()
3507                                                .and_then(|store| store.agent(name.0.as_ref()))
3508                                                .map(|a| a.name().clone())
3509                                        })
3510                                        .unwrap_or_else(|| name.0.clone());
3511                                    AgentMenuItem {
3512                                        id: name.clone(),
3513                                        display_name,
3514                                    }
3515                                })
3516                                .sorted_unstable_by_key(|e| e.display_name.to_lowercase())
3517                                .collect::<Vec<_>>();
3518
3519                            for item in &agent_items {
3520                                let mut entry =
3521                                    ContextMenuEntry::new(item.display_name.clone());
3522
3523                                let icon_path = agent_server_store
3524                                    .agent_icon(&item.id)
3525                                    .or_else(|| {
3526                                        registry_store_ref
3527                                            .as_ref()
3528                                            .and_then(|store| store.agent(item.id.0.as_str()))
3529                                            .and_then(|a| a.icon_path().cloned())
3530                                    });
3531
3532                                if let Some(icon_path) = icon_path {
3533                                    entry = entry.custom_icon_svg(icon_path);
3534                                } else {
3535                                    entry = entry.icon(IconName::Sparkle);
3536                                }
3537
3538                                entry = entry
3539                                    .when(
3540                                        is_agent_selected(AgentType::Custom {
3541                                            name: item.id.0.clone(),
3542                                        }),
3543                                        |this| {
3544                                            this.action(Box::new(
3545                                                NewExternalAgentThread { agent: None },
3546                                            ))
3547                                        },
3548                                    )
3549                                    .icon_color(Color::Muted)
3550                                    .disabled(is_via_collab)
3551                                    .handler({
3552                                        let workspace = workspace.clone();
3553                                        let agent_id = item.id.clone();
3554                                        move |window, cx| {
3555                                            if let Some(workspace) = workspace.upgrade() {
3556                                                workspace.update(cx, |workspace, cx| {
3557                                                    if let Some(panel) =
3558                                                        workspace.panel::<AgentPanel>(cx)
3559                                                    {
3560                                                        panel.update(cx, |panel, cx| {
3561                                                            panel.new_agent_thread(
3562                                                                AgentType::Custom {
3563                                                                    name: agent_id.0.clone(),
3564                                                                },
3565                                                                window,
3566                                                                cx,
3567                                                            );
3568                                                        });
3569                                                    }
3570                                                });
3571                                            }
3572                                        }
3573                                    });
3574
3575                                menu = menu.item(entry);
3576                            }
3577
3578                            menu
3579                        })
3580                        .separator()
3581                        .map(|mut menu| {
3582                            let agent_server_store = agent_server_store.read(cx);
3583                            let registry_store =
3584                                project::AgentRegistryStore::try_global(cx);
3585                            let registry_store_ref =
3586                                registry_store.as_ref().map(|s| s.read(cx));
3587
3588                            let previous_built_in_ids: &[ExternalAgentServerName] =
3589                                &[CLAUDE_AGENT_NAME.into(), CODEX_NAME.into(), GEMINI_NAME.into()];
3590
3591                            let promoted_items = previous_built_in_ids
3592                                .iter()
3593                                .filter(|id| {
3594                                    !agent_server_store.external_agents.contains_key(*id)
3595                                })
3596                                .filter_map(|name| {
3597                                    let display_name = registry_store_ref
3598                                        .as_ref()
3599                                        .and_then(|store| store.agent(name.0.as_ref()))
3600                                        .map(|a| a.name().clone())?;
3601                                    Some((name.clone(), display_name))
3602                                })
3603                                .sorted_unstable_by_key(|(_, display_name)| display_name.to_lowercase())
3604                                .collect::<Vec<_>>();
3605
3606                            for (agent_id, display_name) in &promoted_items {
3607                                let mut entry =
3608                                    ContextMenuEntry::new(display_name.clone());
3609
3610                                let icon_path = registry_store_ref
3611                                    .as_ref()
3612                                    .and_then(|store| store.agent(agent_id.0.as_str()))
3613                                    .and_then(|a| a.icon_path().cloned());
3614
3615                                if let Some(icon_path) = icon_path {
3616                                    entry = entry.custom_icon_svg(icon_path);
3617                                } else {
3618                                    entry = entry.icon(IconName::Sparkle);
3619                                }
3620
3621                                entry = entry
3622                                    .icon_color(Color::Muted)
3623                                    .disabled(is_via_collab)
3624                                    .handler({
3625                                        let workspace = workspace.clone();
3626                                        let agent_id = agent_id.clone();
3627                                        move |window, cx| {
3628                                            let fs = <dyn fs::Fs>::global(cx);
3629                                            let agent_id_string =
3630                                                agent_id.to_string();
3631                                            settings::update_settings_file(
3632                                                fs,
3633                                                cx,
3634                                                move |settings, _| {
3635                                                    let agent_servers = settings
3636                                                        .agent_servers
3637                                                        .get_or_insert_default();
3638                                                    agent_servers.entry(agent_id_string).or_insert_with(|| {
3639                                                        settings::CustomAgentServerSettings::Registry {
3640                                                            default_mode: None,
3641                                                            default_model: None,
3642                                                            env: Default::default(),
3643                                                            favorite_models: Vec::new(),
3644                                                            default_config_options: Default::default(),
3645                                                            favorite_config_option_values: Default::default(),
3646                                                        }
3647                                                    });
3648                                                },
3649                                            );
3650
3651                                            if let Some(workspace) = workspace.upgrade() {
3652                                                workspace.update(cx, |workspace, cx| {
3653                                                    if let Some(panel) =
3654                                                        workspace.panel::<AgentPanel>(cx)
3655                                                    {
3656                                                        panel.update(cx, |panel, cx| {
3657                                                            panel.new_agent_thread(
3658                                                                AgentType::Custom {
3659                                                                    name: agent_id.0.clone(),
3660                                                                },
3661                                                                window,
3662                                                                cx,
3663                                                            );
3664                                                        });
3665                                                    }
3666                                                });
3667                                            }
3668                                        }
3669                                    });
3670
3671                                menu = menu.item(entry);
3672                            }
3673
3674                            menu
3675                        })
3676                        .item(
3677                            ContextMenuEntry::new("Add More Agents")
3678                                .icon(IconName::Plus)
3679                                .icon_color(Color::Muted)
3680                                .handler({
3681                                    move |window, cx| {
3682                                        window.dispatch_action(
3683                                            Box::new(zed_actions::AcpRegistry),
3684                                            cx,
3685                                        )
3686                                    }
3687                                }),
3688                        )
3689                }))
3690            })
3691        };
3692
3693        let is_thread_loading = self
3694            .active_connection_view()
3695            .map(|thread| thread.read(cx).is_loading())
3696            .unwrap_or(false);
3697
3698        let has_custom_icon = selected_agent_custom_icon.is_some();
3699        let selected_agent_custom_icon_for_button = selected_agent_custom_icon.clone();
3700        let selected_agent_builtin_icon = self.selected_agent.icon();
3701        let selected_agent_label_for_tooltip = selected_agent_label.clone();
3702
3703        let selected_agent = div()
3704            .id("selected_agent_icon")
3705            .when_some(selected_agent_custom_icon, |this, icon_path| {
3706                this.px_1()
3707                    .child(Icon::from_external_svg(icon_path).color(Color::Muted))
3708            })
3709            .when(!has_custom_icon, |this| {
3710                this.when_some(self.selected_agent.icon(), |this, icon| {
3711                    this.px_1().child(Icon::new(icon).color(Color::Muted))
3712                })
3713            })
3714            .tooltip(move |_, cx| {
3715                Tooltip::with_meta(
3716                    selected_agent_label_for_tooltip.clone(),
3717                    None,
3718                    "Selected Agent",
3719                    cx,
3720                )
3721            });
3722
3723        let selected_agent = if is_thread_loading {
3724            selected_agent
3725                .with_animation(
3726                    "pulsating-icon",
3727                    Animation::new(Duration::from_secs(1))
3728                        .repeat()
3729                        .with_easing(pulsating_between(0.2, 0.6)),
3730                    |icon, delta| icon.opacity(delta),
3731                )
3732                .into_any_element()
3733        } else {
3734            selected_agent.into_any_element()
3735        };
3736
3737        let show_history_menu = self.history_kind_for_selected_agent(cx).is_some();
3738        let has_v2_flag = cx.has_flag::<AgentV2FeatureFlag>();
3739        let is_empty_state = !self.active_thread_has_messages(cx);
3740
3741        let is_in_history_or_config = matches!(
3742            &self.active_view,
3743            ActiveView::History { .. } | ActiveView::Configuration
3744        );
3745
3746        let use_v2_empty_toolbar = has_v2_flag && is_empty_state && !is_in_history_or_config;
3747
3748        if use_v2_empty_toolbar {
3749            let (chevron_icon, icon_color, label_color) =
3750                if self.new_thread_menu_handle.is_deployed() {
3751                    (IconName::ChevronUp, Color::Accent, Color::Accent)
3752                } else {
3753                    (IconName::ChevronDown, Color::Muted, Color::Default)
3754                };
3755
3756            let agent_icon_element: AnyElement =
3757                if let Some(icon_path) = selected_agent_custom_icon_for_button {
3758                    Icon::from_external_svg(icon_path)
3759                        .size(IconSize::Small)
3760                        .color(icon_color)
3761                        .into_any_element()
3762                } else {
3763                    let icon_name = selected_agent_builtin_icon.unwrap_or(IconName::ZedAgent);
3764                    Icon::new(icon_name)
3765                        .size(IconSize::Small)
3766                        .color(icon_color)
3767                        .into_any_element()
3768                };
3769
3770            let agent_selector_button = ButtonLike::new("agent-selector-trigger")
3771                .selected_style(ButtonStyle::Tinted(TintColor::Accent))
3772                .child(
3773                    h_flex()
3774                        .gap_1()
3775                        .child(agent_icon_element)
3776                        .child(Label::new(selected_agent_label).color(label_color).ml_0p5())
3777                        .child(
3778                            Icon::new(chevron_icon)
3779                                .color(icon_color)
3780                                .size(IconSize::XSmall),
3781                        ),
3782                );
3783
3784            let agent_selector_menu = PopoverMenu::new("new_thread_menu")
3785                .trigger_with_tooltip(agent_selector_button, {
3786                    move |_window, cx| {
3787                        Tooltip::for_action_in(
3788                            "New Thread\u{2026}",
3789                            &ToggleNewThreadMenu,
3790                            &focus_handle,
3791                            cx,
3792                        )
3793                    }
3794                })
3795                .menu({
3796                    let builder = new_thread_menu_builder.clone();
3797                    move |window, cx| builder(window, cx)
3798                })
3799                .with_handle(self.new_thread_menu_handle.clone())
3800                .anchor(Corner::TopLeft)
3801                .offset(gpui::Point {
3802                    x: px(1.0),
3803                    y: px(1.0),
3804                });
3805
3806            h_flex()
3807                .id("agent-panel-toolbar")
3808                .h(Tab::container_height(cx))
3809                .max_w_full()
3810                .flex_none()
3811                .justify_between()
3812                .gap_2()
3813                .bg(cx.theme().colors().tab_bar_background)
3814                .border_b_1()
3815                .border_color(cx.theme().colors().border)
3816                .child(
3817                    h_flex()
3818                        .size_full()
3819                        .gap(DynamicSpacing::Base04.rems(cx))
3820                        .pl(DynamicSpacing::Base04.rems(cx))
3821                        .child(agent_selector_menu)
3822                        .child(self.render_start_thread_in_selector(cx)),
3823                )
3824                .child(
3825                    h_flex()
3826                        .flex_none()
3827                        .gap(DynamicSpacing::Base02.rems(cx))
3828                        .pl(DynamicSpacing::Base04.rems(cx))
3829                        .pr(DynamicSpacing::Base06.rems(cx))
3830                        .when(show_history_menu, |this| {
3831                            this.child(self.render_recent_entries_menu(
3832                                IconName::MenuAltTemp,
3833                                Corner::TopRight,
3834                                cx,
3835                            ))
3836                        })
3837                        .child(self.render_panel_options_menu(window, cx)),
3838                )
3839                .into_any_element()
3840        } else {
3841            let new_thread_menu = PopoverMenu::new("new_thread_menu")
3842                .trigger_with_tooltip(
3843                    IconButton::new("new_thread_menu_btn", IconName::Plus)
3844                        .icon_size(IconSize::Small),
3845                    {
3846                        move |_window, cx| {
3847                            Tooltip::for_action_in(
3848                                "New Thread\u{2026}",
3849                                &ToggleNewThreadMenu,
3850                                &focus_handle,
3851                                cx,
3852                            )
3853                        }
3854                    },
3855                )
3856                .anchor(Corner::TopRight)
3857                .with_handle(self.new_thread_menu_handle.clone())
3858                .menu(move |window, cx| new_thread_menu_builder(window, cx));
3859
3860            h_flex()
3861                .id("agent-panel-toolbar")
3862                .h(Tab::container_height(cx))
3863                .max_w_full()
3864                .flex_none()
3865                .justify_between()
3866                .gap_2()
3867                .bg(cx.theme().colors().tab_bar_background)
3868                .border_b_1()
3869                .border_color(cx.theme().colors().border)
3870                .child(
3871                    h_flex()
3872                        .size_full()
3873                        .gap(DynamicSpacing::Base04.rems(cx))
3874                        .pl(DynamicSpacing::Base04.rems(cx))
3875                        .child(match &self.active_view {
3876                            ActiveView::History { .. } | ActiveView::Configuration => {
3877                                self.render_toolbar_back_button(cx).into_any_element()
3878                            }
3879                            _ => selected_agent.into_any_element(),
3880                        })
3881                        .child(self.render_title_view(window, cx)),
3882                )
3883                .child(
3884                    h_flex()
3885                        .flex_none()
3886                        .gap(DynamicSpacing::Base02.rems(cx))
3887                        .pl(DynamicSpacing::Base04.rems(cx))
3888                        .pr(DynamicSpacing::Base06.rems(cx))
3889                        .child(new_thread_menu)
3890                        .when(show_history_menu, |this| {
3891                            this.child(self.render_recent_entries_menu(
3892                                IconName::MenuAltTemp,
3893                                Corner::TopRight,
3894                                cx,
3895                            ))
3896                        })
3897                        .child(self.render_panel_options_menu(window, cx)),
3898                )
3899                .into_any_element()
3900        }
3901    }
3902
3903    fn render_worktree_creation_status(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
3904        let status = self.worktree_creation_status.as_ref()?;
3905        match status {
3906            WorktreeCreationStatus::Creating => Some(
3907                h_flex()
3908                    .w_full()
3909                    .px(DynamicSpacing::Base06.rems(cx))
3910                    .py(DynamicSpacing::Base02.rems(cx))
3911                    .gap_2()
3912                    .bg(cx.theme().colors().surface_background)
3913                    .border_b_1()
3914                    .border_color(cx.theme().colors().border)
3915                    .child(SpinnerLabel::new().size(LabelSize::Small))
3916                    .child(
3917                        Label::new("Creating worktree…")
3918                            .color(Color::Muted)
3919                            .size(LabelSize::Small),
3920                    )
3921                    .into_any_element(),
3922            ),
3923            WorktreeCreationStatus::Error(message) => Some(
3924                h_flex()
3925                    .w_full()
3926                    .px(DynamicSpacing::Base06.rems(cx))
3927                    .py(DynamicSpacing::Base02.rems(cx))
3928                    .gap_2()
3929                    .bg(cx.theme().colors().surface_background)
3930                    .border_b_1()
3931                    .border_color(cx.theme().colors().border)
3932                    .child(
3933                        Icon::new(IconName::Warning)
3934                            .size(IconSize::Small)
3935                            .color(Color::Warning),
3936                    )
3937                    .child(
3938                        Label::new(message.clone())
3939                            .color(Color::Warning)
3940                            .size(LabelSize::Small)
3941                            .truncate(),
3942                    )
3943                    .into_any_element(),
3944            ),
3945        }
3946    }
3947
3948    fn should_render_trial_end_upsell(&self, cx: &mut Context<Self>) -> bool {
3949        if TrialEndUpsell::dismissed() {
3950            return false;
3951        }
3952
3953        match &self.active_view {
3954            ActiveView::TextThread { .. } => {
3955                if LanguageModelRegistry::global(cx)
3956                    .read(cx)
3957                    .default_model()
3958                    .is_some_and(|model| {
3959                        model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
3960                    })
3961                {
3962                    return false;
3963                }
3964            }
3965            ActiveView::Uninitialized
3966            | ActiveView::AgentThread { .. }
3967            | ActiveView::History { .. }
3968            | ActiveView::Configuration => return false,
3969        }
3970
3971        let plan = self.user_store.read(cx).plan();
3972        let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
3973
3974        plan.is_some_and(|plan| plan == Plan::ZedFree) && has_previous_trial
3975    }
3976
3977    fn should_render_onboarding(&self, cx: &mut Context<Self>) -> bool {
3978        if self.on_boarding_upsell_dismissed.load(Ordering::Acquire) {
3979            return false;
3980        }
3981
3982        let user_store = self.user_store.read(cx);
3983
3984        if user_store.plan().is_some_and(|plan| plan == Plan::ZedPro)
3985            && user_store
3986                .subscription_period()
3987                .and_then(|period| period.0.checked_add_days(chrono::Days::new(1)))
3988                .is_some_and(|date| date < chrono::Utc::now())
3989        {
3990            OnboardingUpsell::set_dismissed(true, cx);
3991            self.on_boarding_upsell_dismissed
3992                .store(true, Ordering::Release);
3993            return false;
3994        }
3995
3996        match &self.active_view {
3997            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {
3998                false
3999            }
4000            ActiveView::AgentThread { server_view, .. }
4001                if server_view.read(cx).as_native_thread(cx).is_none() =>
4002            {
4003                false
4004            }
4005            _ => {
4006                let history_is_empty = self.acp_history.read(cx).is_empty();
4007
4008                let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
4009                    .visible_providers()
4010                    .iter()
4011                    .any(|provider| {
4012                        provider.is_authenticated(cx)
4013                            && provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
4014                    });
4015
4016                history_is_empty || !has_configured_non_zed_providers
4017            }
4018        }
4019    }
4020
4021    fn render_onboarding(
4022        &self,
4023        _window: &mut Window,
4024        cx: &mut Context<Self>,
4025    ) -> Option<impl IntoElement> {
4026        if !self.should_render_onboarding(cx) {
4027            return None;
4028        }
4029
4030        let text_thread_view = matches!(&self.active_view, ActiveView::TextThread { .. });
4031
4032        Some(
4033            div()
4034                .when(text_thread_view, |this| {
4035                    this.bg(cx.theme().colors().editor_background)
4036                })
4037                .child(self.onboarding.clone()),
4038        )
4039    }
4040
4041    fn render_trial_end_upsell(
4042        &self,
4043        _window: &mut Window,
4044        cx: &mut Context<Self>,
4045    ) -> Option<impl IntoElement> {
4046        if !self.should_render_trial_end_upsell(cx) {
4047            return None;
4048        }
4049
4050        Some(
4051            v_flex()
4052                .absolute()
4053                .inset_0()
4054                .size_full()
4055                .bg(cx.theme().colors().panel_background)
4056                .opacity(0.85)
4057                .block_mouse_except_scroll()
4058                .child(EndTrialUpsell::new(Arc::new({
4059                    let this = cx.entity();
4060                    move |_, cx| {
4061                        this.update(cx, |_this, cx| {
4062                            TrialEndUpsell::set_dismissed(true, cx);
4063                            cx.notify();
4064                        });
4065                    }
4066                }))),
4067        )
4068    }
4069
4070    fn emit_configuration_error_telemetry_if_needed(
4071        &mut self,
4072        configuration_error: Option<&ConfigurationError>,
4073    ) {
4074        let error_kind = configuration_error.map(|err| match err {
4075            ConfigurationError::NoProvider => "no_provider",
4076            ConfigurationError::ModelNotFound => "model_not_found",
4077            ConfigurationError::ProviderNotAuthenticated(_) => "provider_not_authenticated",
4078        });
4079
4080        let error_kind_string = error_kind.map(String::from);
4081
4082        if self.last_configuration_error_telemetry == error_kind_string {
4083            return;
4084        }
4085
4086        self.last_configuration_error_telemetry = error_kind_string;
4087
4088        if let Some(kind) = error_kind {
4089            let message = configuration_error
4090                .map(|err| err.to_string())
4091                .unwrap_or_default();
4092
4093            telemetry::event!("Agent Panel Error Shown", kind = kind, message = message,);
4094        }
4095    }
4096
4097    fn render_configuration_error(
4098        &self,
4099        border_bottom: bool,
4100        configuration_error: &ConfigurationError,
4101        focus_handle: &FocusHandle,
4102        cx: &mut App,
4103    ) -> impl IntoElement {
4104        let zed_provider_configured = AgentSettings::get_global(cx)
4105            .default_model
4106            .as_ref()
4107            .is_some_and(|selection| selection.provider.0.as_str() == "zed.dev");
4108
4109        let callout = if zed_provider_configured {
4110            Callout::new()
4111                .icon(IconName::Warning)
4112                .severity(Severity::Warning)
4113                .when(border_bottom, |this| {
4114                    this.border_position(ui::BorderPosition::Bottom)
4115                })
4116                .title("Sign in to continue using Zed as your LLM provider.")
4117                .actions_slot(
4118                    Button::new("sign_in", "Sign In")
4119                        .style(ButtonStyle::Tinted(ui::TintColor::Warning))
4120                        .label_size(LabelSize::Small)
4121                        .on_click({
4122                            let workspace = self.workspace.clone();
4123                            move |_, _, cx| {
4124                                let Ok(client) =
4125                                    workspace.update(cx, |workspace, _| workspace.client().clone())
4126                                else {
4127                                    return;
4128                                };
4129
4130                                cx.spawn(async move |cx| {
4131                                    client.sign_in_with_optional_connect(true, cx).await
4132                                })
4133                                .detach_and_log_err(cx);
4134                            }
4135                        }),
4136                )
4137        } else {
4138            Callout::new()
4139                .icon(IconName::Warning)
4140                .severity(Severity::Warning)
4141                .when(border_bottom, |this| {
4142                    this.border_position(ui::BorderPosition::Bottom)
4143                })
4144                .title(configuration_error.to_string())
4145                .actions_slot(
4146                    Button::new("settings", "Configure")
4147                        .style(ButtonStyle::Tinted(ui::TintColor::Warning))
4148                        .label_size(LabelSize::Small)
4149                        .key_binding(
4150                            KeyBinding::for_action_in(&OpenSettings, focus_handle, cx)
4151                                .map(|kb| kb.size(rems_from_px(12.))),
4152                        )
4153                        .on_click(|_event, window, cx| {
4154                            window.dispatch_action(OpenSettings.boxed_clone(), cx)
4155                        }),
4156                )
4157        };
4158
4159        match configuration_error {
4160            ConfigurationError::ModelNotFound
4161            | ConfigurationError::ProviderNotAuthenticated(_)
4162            | ConfigurationError::NoProvider => callout.into_any_element(),
4163        }
4164    }
4165
4166    fn render_text_thread(
4167        &self,
4168        text_thread_editor: &Entity<TextThreadEditor>,
4169        buffer_search_bar: &Entity<BufferSearchBar>,
4170        window: &mut Window,
4171        cx: &mut Context<Self>,
4172    ) -> Div {
4173        let mut registrar = buffer_search::DivRegistrar::new(
4174            |this, _, _cx| match &this.active_view {
4175                ActiveView::TextThread {
4176                    buffer_search_bar, ..
4177                } => Some(buffer_search_bar.clone()),
4178                _ => None,
4179            },
4180            cx,
4181        );
4182        BufferSearchBar::register(&mut registrar);
4183        registrar
4184            .into_div()
4185            .size_full()
4186            .relative()
4187            .map(|parent| {
4188                buffer_search_bar.update(cx, |buffer_search_bar, cx| {
4189                    if buffer_search_bar.is_dismissed() {
4190                        return parent;
4191                    }
4192                    parent.child(
4193                        div()
4194                            .p(DynamicSpacing::Base08.rems(cx))
4195                            .border_b_1()
4196                            .border_color(cx.theme().colors().border_variant)
4197                            .bg(cx.theme().colors().editor_background)
4198                            .child(buffer_search_bar.render(window, cx)),
4199                    )
4200                })
4201            })
4202            .child(text_thread_editor.clone())
4203            .child(self.render_drag_target(cx))
4204    }
4205
4206    fn render_drag_target(&self, cx: &Context<Self>) -> Div {
4207        let is_local = self.project.read(cx).is_local();
4208        div()
4209            .invisible()
4210            .absolute()
4211            .top_0()
4212            .right_0()
4213            .bottom_0()
4214            .left_0()
4215            .bg(cx.theme().colors().drop_target_background)
4216            .drag_over::<DraggedTab>(|this, _, _, _| this.visible())
4217            .drag_over::<DraggedSelection>(|this, _, _, _| this.visible())
4218            .when(is_local, |this| {
4219                this.drag_over::<ExternalPaths>(|this, _, _, _| this.visible())
4220            })
4221            .on_drop(cx.listener(move |this, tab: &DraggedTab, window, cx| {
4222                let item = tab.pane.read(cx).item_for_index(tab.ix);
4223                let project_paths = item
4224                    .and_then(|item| item.project_path(cx))
4225                    .into_iter()
4226                    .collect::<Vec<_>>();
4227                this.handle_drop(project_paths, vec![], window, cx);
4228            }))
4229            .on_drop(
4230                cx.listener(move |this, selection: &DraggedSelection, window, cx| {
4231                    let project_paths = selection
4232                        .items()
4233                        .filter_map(|item| this.project.read(cx).path_for_entry(item.entry_id, cx))
4234                        .collect::<Vec<_>>();
4235                    this.handle_drop(project_paths, vec![], window, cx);
4236                }),
4237            )
4238            .on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| {
4239                let tasks = paths
4240                    .paths()
4241                    .iter()
4242                    .map(|path| {
4243                        Workspace::project_path_for_path(this.project.clone(), path, false, cx)
4244                    })
4245                    .collect::<Vec<_>>();
4246                cx.spawn_in(window, async move |this, cx| {
4247                    let mut paths = vec![];
4248                    let mut added_worktrees = vec![];
4249                    let opened_paths = futures::future::join_all(tasks).await;
4250                    for entry in opened_paths {
4251                        if let Some((worktree, project_path)) = entry.log_err() {
4252                            added_worktrees.push(worktree);
4253                            paths.push(project_path);
4254                        }
4255                    }
4256                    this.update_in(cx, |this, window, cx| {
4257                        this.handle_drop(paths, added_worktrees, window, cx);
4258                    })
4259                    .ok();
4260                })
4261                .detach();
4262            }))
4263    }
4264
4265    fn handle_drop(
4266        &mut self,
4267        paths: Vec<ProjectPath>,
4268        added_worktrees: Vec<Entity<Worktree>>,
4269        window: &mut Window,
4270        cx: &mut Context<Self>,
4271    ) {
4272        match &self.active_view {
4273            ActiveView::AgentThread { server_view } => {
4274                server_view.update(cx, |thread_view, cx| {
4275                    thread_view.insert_dragged_files(paths, added_worktrees, window, cx);
4276                });
4277            }
4278            ActiveView::TextThread {
4279                text_thread_editor, ..
4280            } => {
4281                text_thread_editor.update(cx, |text_thread_editor, cx| {
4282                    TextThreadEditor::insert_dragged_files(
4283                        text_thread_editor,
4284                        paths,
4285                        added_worktrees,
4286                        window,
4287                        cx,
4288                    );
4289                });
4290            }
4291            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {}
4292        }
4293    }
4294
4295    fn render_workspace_trust_message(&self, cx: &Context<Self>) -> Option<impl IntoElement> {
4296        if !self.show_trust_workspace_message {
4297            return None;
4298        }
4299
4300        let description = "To protect your system, third-party code—like MCP servers—won't run until you mark this workspace as safe.";
4301
4302        Some(
4303            Callout::new()
4304                .icon(IconName::Warning)
4305                .severity(Severity::Warning)
4306                .border_position(ui::BorderPosition::Bottom)
4307                .title("You're in Restricted Mode")
4308                .description(description)
4309                .actions_slot(
4310                    Button::new("open-trust-modal", "Configure Project Trust")
4311                        .label_size(LabelSize::Small)
4312                        .style(ButtonStyle::Outlined)
4313                        .on_click({
4314                            cx.listener(move |this, _, window, cx| {
4315                                this.workspace
4316                                    .update(cx, |workspace, cx| {
4317                                        workspace
4318                                            .show_worktree_trust_security_modal(true, window, cx)
4319                                    })
4320                                    .log_err();
4321                            })
4322                        }),
4323                ),
4324        )
4325    }
4326
4327    fn key_context(&self) -> KeyContext {
4328        let mut key_context = KeyContext::new_with_defaults();
4329        key_context.add("AgentPanel");
4330        match &self.active_view {
4331            ActiveView::AgentThread { .. } => key_context.add("acp_thread"),
4332            ActiveView::TextThread { .. } => key_context.add("text_thread"),
4333            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {}
4334        }
4335        key_context
4336    }
4337}
4338
4339impl Render for AgentPanel {
4340    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4341        // WARNING: Changes to this element hierarchy can have
4342        // non-obvious implications to the layout of children.
4343        //
4344        // If you need to change it, please confirm:
4345        // - The message editor expands (cmd-option-esc) correctly
4346        // - When expanded, the buttons at the bottom of the panel are displayed correctly
4347        // - Font size works as expected and can be changed with cmd-+/cmd-
4348        // - Scrolling in all views works as expected
4349        // - Files can be dropped into the panel
4350        let content = v_flex()
4351            .relative()
4352            .size_full()
4353            .justify_between()
4354            .key_context(self.key_context())
4355            .on_action(cx.listener(|this, action: &NewThread, window, cx| {
4356                this.new_thread(action, window, cx);
4357            }))
4358            .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
4359                this.open_history(window, cx);
4360            }))
4361            .on_action(cx.listener(|this, _: &OpenSettings, window, cx| {
4362                this.open_configuration(window, cx);
4363            }))
4364            .on_action(cx.listener(Self::open_active_thread_as_markdown))
4365            .on_action(cx.listener(Self::deploy_rules_library))
4366            .on_action(cx.listener(Self::go_back))
4367            .on_action(cx.listener(Self::toggle_navigation_menu))
4368            .on_action(cx.listener(Self::toggle_options_menu))
4369            .on_action(cx.listener(Self::toggle_start_thread_in_selector))
4370            .on_action(cx.listener(Self::increase_font_size))
4371            .on_action(cx.listener(Self::decrease_font_size))
4372            .on_action(cx.listener(Self::reset_font_size))
4373            .on_action(cx.listener(Self::toggle_zoom))
4374            .on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| {
4375                if let Some(thread_view) = this.active_connection_view() {
4376                    thread_view.update(cx, |thread_view, cx| thread_view.reauthenticate(window, cx))
4377                }
4378            }))
4379            .child(self.render_toolbar(window, cx))
4380            .children(self.render_worktree_creation_status(cx))
4381            .children(self.render_workspace_trust_message(cx))
4382            .children(self.render_onboarding(window, cx))
4383            .map(|parent| {
4384                // Emit configuration error telemetry before entering the match to avoid borrow conflicts
4385                if matches!(&self.active_view, ActiveView::TextThread { .. }) {
4386                    let model_registry = LanguageModelRegistry::read_global(cx);
4387                    let configuration_error =
4388                        model_registry.configuration_error(model_registry.default_model(), cx);
4389                    self.emit_configuration_error_telemetry_if_needed(configuration_error.as_ref());
4390                }
4391
4392                match &self.active_view {
4393                    ActiveView::Uninitialized => parent,
4394                    ActiveView::AgentThread { server_view, .. } => parent
4395                        .child(server_view.clone())
4396                        .child(self.render_drag_target(cx)),
4397                    ActiveView::History { kind } => match kind {
4398                        HistoryKind::AgentThreads => parent.child(self.acp_history.clone()),
4399                        HistoryKind::TextThreads => parent.child(self.text_thread_history.clone()),
4400                    },
4401                    ActiveView::TextThread {
4402                        text_thread_editor,
4403                        buffer_search_bar,
4404                        ..
4405                    } => {
4406                        let model_registry = LanguageModelRegistry::read_global(cx);
4407                        let configuration_error =
4408                            model_registry.configuration_error(model_registry.default_model(), cx);
4409
4410                        parent
4411                            .map(|this| {
4412                                if !self.should_render_onboarding(cx)
4413                                    && let Some(err) = configuration_error.as_ref()
4414                                {
4415                                    this.child(self.render_configuration_error(
4416                                        true,
4417                                        err,
4418                                        &self.focus_handle(cx),
4419                                        cx,
4420                                    ))
4421                                } else {
4422                                    this
4423                                }
4424                            })
4425                            .child(self.render_text_thread(
4426                                text_thread_editor,
4427                                buffer_search_bar,
4428                                window,
4429                                cx,
4430                            ))
4431                    }
4432                    ActiveView::Configuration => parent.children(self.configuration.clone()),
4433                }
4434            })
4435            .children(self.render_trial_end_upsell(window, cx));
4436
4437        match self.active_view.which_font_size_used() {
4438            WhichFontSize::AgentFont => {
4439                WithRemSize::new(ThemeSettings::get_global(cx).agent_ui_font_size(cx))
4440                    .size_full()
4441                    .child(content)
4442                    .into_any()
4443            }
4444            _ => content.into_any(),
4445        }
4446    }
4447}
4448
4449struct PromptLibraryInlineAssist {
4450    workspace: WeakEntity<Workspace>,
4451}
4452
4453impl PromptLibraryInlineAssist {
4454    pub fn new(workspace: WeakEntity<Workspace>) -> Self {
4455        Self { workspace }
4456    }
4457}
4458
4459impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
4460    fn assist(
4461        &self,
4462        prompt_editor: &Entity<Editor>,
4463        initial_prompt: Option<String>,
4464        window: &mut Window,
4465        cx: &mut Context<RulesLibrary>,
4466    ) {
4467        InlineAssistant::update_global(cx, |assistant, cx| {
4468            let Some(workspace) = self.workspace.upgrade() else {
4469                return;
4470            };
4471            let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) else {
4472                return;
4473            };
4474            let project = workspace.read(cx).project().downgrade();
4475            let panel = panel.read(cx);
4476            let thread_store = panel.thread_store().clone();
4477            let history = panel.history().downgrade();
4478            assistant.assist(
4479                prompt_editor,
4480                self.workspace.clone(),
4481                project,
4482                thread_store,
4483                None,
4484                history,
4485                initial_prompt,
4486                window,
4487                cx,
4488            );
4489        })
4490    }
4491
4492    fn focus_agent_panel(
4493        &self,
4494        workspace: &mut Workspace,
4495        window: &mut Window,
4496        cx: &mut Context<Workspace>,
4497    ) -> bool {
4498        workspace.focus_panel::<AgentPanel>(window, cx).is_some()
4499    }
4500}
4501
4502pub struct ConcreteAssistantPanelDelegate;
4503
4504impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
4505    fn active_text_thread_editor(
4506        &self,
4507        workspace: &mut Workspace,
4508        _window: &mut Window,
4509        cx: &mut Context<Workspace>,
4510    ) -> Option<Entity<TextThreadEditor>> {
4511        let panel = workspace.panel::<AgentPanel>(cx)?;
4512        panel.read(cx).active_text_thread_editor()
4513    }
4514
4515    fn open_local_text_thread(
4516        &self,
4517        workspace: &mut Workspace,
4518        path: Arc<Path>,
4519        window: &mut Window,
4520        cx: &mut Context<Workspace>,
4521    ) -> Task<Result<()>> {
4522        let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
4523            return Task::ready(Err(anyhow!("Agent panel not found")));
4524        };
4525
4526        panel.update(cx, |panel, cx| {
4527            panel.open_saved_text_thread(path, window, cx)
4528        })
4529    }
4530
4531    fn open_remote_text_thread(
4532        &self,
4533        _workspace: &mut Workspace,
4534        _text_thread_id: assistant_text_thread::TextThreadId,
4535        _window: &mut Window,
4536        _cx: &mut Context<Workspace>,
4537    ) -> Task<Result<Entity<TextThreadEditor>>> {
4538        Task::ready(Err(anyhow!("opening remote context not implemented")))
4539    }
4540
4541    fn quote_selection(
4542        &self,
4543        workspace: &mut Workspace,
4544        selection_ranges: Vec<Range<Anchor>>,
4545        buffer: Entity<MultiBuffer>,
4546        window: &mut Window,
4547        cx: &mut Context<Workspace>,
4548    ) {
4549        let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
4550            return;
4551        };
4552
4553        if !panel.focus_handle(cx).contains_focused(window, cx) {
4554            workspace.toggle_panel_focus::<AgentPanel>(window, cx);
4555        }
4556
4557        panel.update(cx, |_, cx| {
4558            // Wait to create a new context until the workspace is no longer
4559            // being updated.
4560            cx.defer_in(window, move |panel, window, cx| {
4561                if let Some(thread_view) = panel.active_connection_view() {
4562                    thread_view.update(cx, |thread_view, cx| {
4563                        thread_view.insert_selections(window, cx);
4564                    });
4565                } else if let Some(text_thread_editor) = panel.active_text_thread_editor() {
4566                    let snapshot = buffer.read(cx).snapshot(cx);
4567                    let selection_ranges = selection_ranges
4568                        .into_iter()
4569                        .map(|range| range.to_point(&snapshot))
4570                        .collect::<Vec<_>>();
4571
4572                    text_thread_editor.update(cx, |text_thread_editor, cx| {
4573                        text_thread_editor.quote_ranges(selection_ranges, snapshot, window, cx)
4574                    });
4575                }
4576            });
4577        });
4578    }
4579
4580    fn quote_terminal_text(
4581        &self,
4582        workspace: &mut Workspace,
4583        text: String,
4584        window: &mut Window,
4585        cx: &mut Context<Workspace>,
4586    ) {
4587        let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
4588            return;
4589        };
4590
4591        if !panel.focus_handle(cx).contains_focused(window, cx) {
4592            workspace.toggle_panel_focus::<AgentPanel>(window, cx);
4593        }
4594
4595        panel.update(cx, |_, cx| {
4596            // Wait to create a new context until the workspace is no longer
4597            // being updated.
4598            cx.defer_in(window, move |panel, window, cx| {
4599                if let Some(thread_view) = panel.active_connection_view() {
4600                    thread_view.update(cx, |thread_view, cx| {
4601                        thread_view.insert_terminal_text(text, window, cx);
4602                    });
4603                } else if let Some(text_thread_editor) = panel.active_text_thread_editor() {
4604                    text_thread_editor.update(cx, |text_thread_editor, cx| {
4605                        text_thread_editor.quote_terminal_text(text, window, cx)
4606                    });
4607                }
4608            });
4609        });
4610    }
4611}
4612
4613struct OnboardingUpsell;
4614
4615impl Dismissable for OnboardingUpsell {
4616    const KEY: &'static str = "dismissed-trial-upsell";
4617}
4618
4619struct TrialEndUpsell;
4620
4621impl Dismissable for TrialEndUpsell {
4622    const KEY: &'static str = "dismissed-trial-end-upsell";
4623}
4624
4625/// Test-only helper methods
4626#[cfg(any(test, feature = "test-support"))]
4627impl AgentPanel {
4628    pub fn test_new(
4629        workspace: &Workspace,
4630        text_thread_store: Entity<assistant_text_thread::TextThreadStore>,
4631        window: &mut Window,
4632        cx: &mut Context<Self>,
4633    ) -> Self {
4634        Self::new(workspace, text_thread_store, None, window, cx)
4635    }
4636
4637    /// Opens an external thread using an arbitrary AgentServer.
4638    ///
4639    /// This is a test-only helper that allows visual tests and integration tests
4640    /// to inject a stub server without modifying production code paths.
4641    /// Not compiled into production builds.
4642    pub fn open_external_thread_with_server(
4643        &mut self,
4644        server: Rc<dyn AgentServer>,
4645        window: &mut Window,
4646        cx: &mut Context<Self>,
4647    ) {
4648        let workspace = self.workspace.clone();
4649        let project = self.project.clone();
4650
4651        let ext_agent = ExternalAgent::Custom {
4652            name: server.name(),
4653        };
4654
4655        self.create_external_thread(
4656            server, None, None, None, None, workspace, project, ext_agent, true, window, cx,
4657        );
4658    }
4659
4660    /// Returns the currently active thread view, if any.
4661    ///
4662    /// This is a test-only accessor that exposes the private `active_thread_view()`
4663    /// method for test assertions. Not compiled into production builds.
4664    pub fn active_thread_view_for_tests(&self) -> Option<&Entity<ConnectionView>> {
4665        self.active_connection_view()
4666    }
4667
4668    /// Sets the start_thread_in value directly, bypassing validation.
4669    ///
4670    /// This is a test-only helper for visual tests that need to show specific
4671    /// start_thread_in states without requiring a real git repository.
4672    pub fn set_start_thread_in_for_tests(&mut self, target: StartThreadIn, cx: &mut Context<Self>) {
4673        self.start_thread_in = target;
4674        cx.notify();
4675    }
4676
4677    /// Returns the current worktree creation status.
4678    ///
4679    /// This is a test-only helper for visual tests.
4680    pub fn worktree_creation_status_for_tests(&self) -> Option<&WorktreeCreationStatus> {
4681        self.worktree_creation_status.as_ref()
4682    }
4683
4684    /// Sets the worktree creation status directly.
4685    ///
4686    /// This is a test-only helper for visual tests that need to show the
4687    /// "Creating worktree…" spinner or error banners.
4688    pub fn set_worktree_creation_status_for_tests(
4689        &mut self,
4690        status: Option<WorktreeCreationStatus>,
4691        cx: &mut Context<Self>,
4692    ) {
4693        self.worktree_creation_status = status;
4694        cx.notify();
4695    }
4696
4697    /// Opens the history view.
4698    ///
4699    /// This is a test-only helper that exposes the private `open_history()`
4700    /// method for visual tests.
4701    pub fn open_history_for_tests(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4702        self.open_history(window, cx);
4703    }
4704
4705    /// Opens the start_thread_in selector popover menu.
4706    ///
4707    /// This is a test-only helper for visual tests.
4708    pub fn open_start_thread_in_menu_for_tests(
4709        &mut self,
4710        window: &mut Window,
4711        cx: &mut Context<Self>,
4712    ) {
4713        self.start_thread_in_menu_handle.show(window, cx);
4714    }
4715
4716    /// Dismisses the start_thread_in dropdown menu.
4717    ///
4718    /// This is a test-only helper for visual tests.
4719    pub fn close_start_thread_in_menu_for_tests(&mut self, cx: &mut Context<Self>) {
4720        self.start_thread_in_menu_handle.hide(cx);
4721    }
4722}
4723
4724#[cfg(test)]
4725mod tests {
4726    use super::*;
4727    use crate::connection_view::tests::{StubAgentServer, init_test};
4728    use crate::test_support::{active_session_id, open_thread_with_connection, send_message};
4729    use acp_thread::{StubAgentConnection, ThreadStatus};
4730    use assistant_text_thread::TextThreadStore;
4731    use feature_flags::FeatureFlagAppExt;
4732    use fs::FakeFs;
4733    use gpui::{TestAppContext, VisualTestContext};
4734    use project::Project;
4735    use serde_json::json;
4736    use workspace::MultiWorkspace;
4737
4738    #[gpui::test]
4739    async fn test_active_thread_serialize_and_load_round_trip(cx: &mut TestAppContext) {
4740        init_test(cx);
4741        cx.update(|cx| {
4742            cx.update_flags(true, vec!["agent-v2".to_string()]);
4743            agent::ThreadStore::init_global(cx);
4744            language_model::LanguageModelRegistry::test(cx);
4745        });
4746
4747        // --- Create a MultiWorkspace window with two workspaces ---
4748        let fs = FakeFs::new(cx.executor());
4749        let project_a = Project::test(fs.clone(), [], cx).await;
4750        let project_b = Project::test(fs, [], cx).await;
4751
4752        let multi_workspace =
4753            cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
4754
4755        let workspace_a = multi_workspace
4756            .read_with(cx, |multi_workspace, _cx| {
4757                multi_workspace.workspace().clone()
4758            })
4759            .unwrap();
4760
4761        let workspace_b = multi_workspace
4762            .update(cx, |multi_workspace, window, cx| {
4763                multi_workspace.test_add_workspace(project_b.clone(), window, cx)
4764            })
4765            .unwrap();
4766
4767        workspace_a.update(cx, |workspace, _cx| {
4768            workspace.set_random_database_id();
4769        });
4770        workspace_b.update(cx, |workspace, _cx| {
4771            workspace.set_random_database_id();
4772        });
4773
4774        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
4775
4776        // --- Set up workspace A: width=300, with an active thread ---
4777        let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
4778            let text_thread_store = cx.new(|cx| TextThreadStore::fake(project_a.clone(), cx));
4779            cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx))
4780        });
4781
4782        panel_a.update(cx, |panel, _cx| {
4783            panel.width = Some(px(300.0));
4784        });
4785
4786        panel_a.update_in(cx, |panel, window, cx| {
4787            panel.open_external_thread_with_server(
4788                Rc::new(StubAgentServer::default_response()),
4789                window,
4790                cx,
4791            );
4792        });
4793
4794        cx.run_until_parked();
4795
4796        panel_a.read_with(cx, |panel, cx| {
4797            assert!(
4798                panel.active_agent_thread(cx).is_some(),
4799                "workspace A should have an active thread after connection"
4800            );
4801        });
4802
4803        let agent_type_a = panel_a.read_with(cx, |panel, _cx| panel.selected_agent.clone());
4804
4805        // --- Set up workspace B: ClaudeCode, width=400, no active thread ---
4806        let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
4807            let text_thread_store = cx.new(|cx| TextThreadStore::fake(project_b.clone(), cx));
4808            cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx))
4809        });
4810
4811        panel_b.update(cx, |panel, _cx| {
4812            panel.width = Some(px(400.0));
4813            panel.selected_agent = AgentType::Custom {
4814                name: "claude-acp".into(),
4815            };
4816        });
4817
4818        // --- Serialize both panels ---
4819        panel_a.update(cx, |panel, cx| panel.serialize(cx));
4820        panel_b.update(cx, |panel, cx| panel.serialize(cx));
4821        cx.run_until_parked();
4822
4823        // --- Load fresh panels for each workspace and verify independent state ---
4824        let prompt_builder = Arc::new(prompt_store::PromptBuilder::new(None).unwrap());
4825
4826        let async_cx = cx.update(|window, cx| window.to_async(cx));
4827        let loaded_a = AgentPanel::load(workspace_a.downgrade(), prompt_builder.clone(), async_cx)
4828            .await
4829            .expect("panel A load should succeed");
4830        cx.run_until_parked();
4831
4832        let async_cx = cx.update(|window, cx| window.to_async(cx));
4833        let loaded_b = AgentPanel::load(workspace_b.downgrade(), prompt_builder.clone(), async_cx)
4834            .await
4835            .expect("panel B load should succeed");
4836        cx.run_until_parked();
4837
4838        // Workspace A should restore its thread, width, and agent type
4839        loaded_a.read_with(cx, |panel, _cx| {
4840            assert_eq!(
4841                panel.width,
4842                Some(px(300.0)),
4843                "workspace A width should be restored"
4844            );
4845            assert_eq!(
4846                panel.selected_agent, agent_type_a,
4847                "workspace A agent type should be restored"
4848            );
4849            assert!(
4850                panel.active_connection_view().is_some(),
4851                "workspace A should have its active thread restored"
4852            );
4853        });
4854
4855        // Workspace B should restore its own width and agent type, with no thread
4856        loaded_b.read_with(cx, |panel, _cx| {
4857            assert_eq!(
4858                panel.width,
4859                Some(px(400.0)),
4860                "workspace B width should be restored"
4861            );
4862            assert_eq!(
4863                panel.selected_agent,
4864                AgentType::Custom {
4865                    name: "claude-acp".into()
4866                },
4867                "workspace B agent type should be restored"
4868            );
4869            assert!(
4870                panel.active_connection_view().is_none(),
4871                "workspace B should have no active thread"
4872            );
4873        });
4874    }
4875
4876    // Simple regression test
4877    #[gpui::test]
4878    async fn test_new_text_thread_action_handler(cx: &mut TestAppContext) {
4879        init_test(cx);
4880
4881        let fs = FakeFs::new(cx.executor());
4882
4883        cx.update(|cx| {
4884            cx.update_flags(true, vec!["agent-v2".to_string()]);
4885            agent::ThreadStore::init_global(cx);
4886            language_model::LanguageModelRegistry::test(cx);
4887            let slash_command_registry =
4888                assistant_slash_command::SlashCommandRegistry::default_global(cx);
4889            slash_command_registry
4890                .register_command(assistant_slash_commands::DefaultSlashCommand, false);
4891            <dyn fs::Fs>::set_global(fs.clone(), cx);
4892        });
4893
4894        let project = Project::test(fs.clone(), [], cx).await;
4895
4896        let multi_workspace =
4897            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4898
4899        let workspace_a = multi_workspace
4900            .read_with(cx, |multi_workspace, _cx| {
4901                multi_workspace.workspace().clone()
4902            })
4903            .unwrap();
4904
4905        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
4906
4907        workspace_a.update_in(cx, |workspace, window, cx| {
4908            let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
4909            let panel =
4910                cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx));
4911            workspace.add_panel(panel, window, cx);
4912        });
4913
4914        cx.run_until_parked();
4915
4916        workspace_a.update_in(cx, |_, window, cx| {
4917            window.dispatch_action(NewTextThread.boxed_clone(), cx);
4918        });
4919
4920        cx.run_until_parked();
4921    }
4922
4923    async fn setup_panel(cx: &mut TestAppContext) -> (Entity<AgentPanel>, VisualTestContext) {
4924        init_test(cx);
4925        cx.update(|cx| {
4926            cx.update_flags(true, vec!["agent-v2".to_string()]);
4927            agent::ThreadStore::init_global(cx);
4928            language_model::LanguageModelRegistry::test(cx);
4929        });
4930
4931        let fs = FakeFs::new(cx.executor());
4932        let project = Project::test(fs.clone(), [], cx).await;
4933
4934        let multi_workspace =
4935            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4936
4937        let workspace = multi_workspace
4938            .read_with(cx, |mw, _cx| mw.workspace().clone())
4939            .unwrap();
4940
4941        let mut cx = VisualTestContext::from_window(multi_workspace.into(), cx);
4942
4943        let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
4944            let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
4945            cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx))
4946        });
4947
4948        (panel, cx)
4949    }
4950
4951    #[gpui::test]
4952    async fn test_running_thread_retained_when_navigating_away(cx: &mut TestAppContext) {
4953        let (panel, mut cx) = setup_panel(cx).await;
4954
4955        let connection_a = StubAgentConnection::new();
4956        open_thread_with_connection(&panel, connection_a.clone(), &mut cx);
4957        send_message(&panel, &mut cx);
4958
4959        let session_id_a = active_session_id(&panel, &cx);
4960
4961        // Send a chunk to keep thread A generating (don't end the turn).
4962        cx.update(|_, cx| {
4963            connection_a.send_update(
4964                session_id_a.clone(),
4965                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("chunk".into())),
4966                cx,
4967            );
4968        });
4969        cx.run_until_parked();
4970
4971        // Verify thread A is generating.
4972        panel.read_with(&cx, |panel, cx| {
4973            let thread = panel.active_agent_thread(cx).unwrap();
4974            assert_eq!(thread.read(cx).status(), ThreadStatus::Generating);
4975            assert!(panel.background_threads.is_empty());
4976        });
4977
4978        // Open a new thread B — thread A should be retained in background.
4979        let connection_b = StubAgentConnection::new();
4980        open_thread_with_connection(&panel, connection_b, &mut cx);
4981
4982        panel.read_with(&cx, |panel, _cx| {
4983            assert_eq!(
4984                panel.background_threads.len(),
4985                1,
4986                "Running thread A should be retained in background_views"
4987            );
4988            assert!(
4989                panel.background_threads.contains_key(&session_id_a),
4990                "Background view should be keyed by thread A's session ID"
4991            );
4992        });
4993    }
4994
4995    #[gpui::test]
4996    async fn test_idle_thread_dropped_when_navigating_away(cx: &mut TestAppContext) {
4997        let (panel, mut cx) = setup_panel(cx).await;
4998
4999        let connection_a = StubAgentConnection::new();
5000        connection_a.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
5001            acp::ContentChunk::new("Response".into()),
5002        )]);
5003        open_thread_with_connection(&panel, connection_a, &mut cx);
5004        send_message(&panel, &mut cx);
5005
5006        let weak_view_a = panel.read_with(&cx, |panel, _cx| {
5007            panel.active_connection_view().unwrap().downgrade()
5008        });
5009
5010        // Thread A should be idle (auto-completed via set_next_prompt_updates).
5011        panel.read_with(&cx, |panel, cx| {
5012            let thread = panel.active_agent_thread(cx).unwrap();
5013            assert_eq!(thread.read(cx).status(), ThreadStatus::Idle);
5014        });
5015
5016        // Open a new thread B — thread A should NOT be retained.
5017        let connection_b = StubAgentConnection::new();
5018        open_thread_with_connection(&panel, connection_b, &mut cx);
5019
5020        panel.read_with(&cx, |panel, _cx| {
5021            assert!(
5022                panel.background_threads.is_empty(),
5023                "Idle thread A should not be retained in background_views"
5024            );
5025        });
5026
5027        // Verify the old ConnectionView entity was dropped (no strong references remain).
5028        assert!(
5029            weak_view_a.upgrade().is_none(),
5030            "Idle ConnectionView should have been dropped"
5031        );
5032    }
5033
5034    #[gpui::test]
5035    async fn test_background_thread_promoted_via_load(cx: &mut TestAppContext) {
5036        let (panel, mut cx) = setup_panel(cx).await;
5037
5038        let connection_a = StubAgentConnection::new();
5039        open_thread_with_connection(&panel, connection_a.clone(), &mut cx);
5040        send_message(&panel, &mut cx);
5041
5042        let session_id_a = active_session_id(&panel, &cx);
5043
5044        // Keep thread A generating.
5045        cx.update(|_, cx| {
5046            connection_a.send_update(
5047                session_id_a.clone(),
5048                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("chunk".into())),
5049                cx,
5050            );
5051        });
5052        cx.run_until_parked();
5053
5054        // Open thread B — thread A goes to background.
5055        let connection_b = StubAgentConnection::new();
5056        open_thread_with_connection(&panel, connection_b, &mut cx);
5057
5058        let session_id_b = active_session_id(&panel, &cx);
5059
5060        panel.read_with(&cx, |panel, _cx| {
5061            assert_eq!(panel.background_threads.len(), 1);
5062            assert!(panel.background_threads.contains_key(&session_id_a));
5063        });
5064
5065        // Load thread A back via load_agent_thread — should promote from background.
5066        panel.update_in(&mut cx, |panel, window, cx| {
5067            panel.load_agent_thread(session_id_a.clone(), None, None, window, cx);
5068        });
5069
5070        // Thread A should now be the active view, promoted from background.
5071        let active_session = active_session_id(&panel, &cx);
5072        assert_eq!(
5073            active_session, session_id_a,
5074            "Thread A should be the active thread after promotion"
5075        );
5076
5077        panel.read_with(&cx, |panel, _cx| {
5078            assert!(
5079                !panel.background_threads.contains_key(&session_id_a),
5080                "Promoted thread A should no longer be in background_views"
5081            );
5082            assert!(
5083                !panel.background_threads.contains_key(&session_id_b),
5084                "Thread B (idle) should not have been retained in background_views"
5085            );
5086        });
5087    }
5088
5089    #[gpui::test]
5090    async fn test_thread_target_local_project(cx: &mut TestAppContext) {
5091        init_test(cx);
5092        cx.update(|cx| {
5093            cx.update_flags(true, vec!["agent-v2".to_string()]);
5094            agent::ThreadStore::init_global(cx);
5095            language_model::LanguageModelRegistry::test(cx);
5096        });
5097
5098        let fs = FakeFs::new(cx.executor());
5099        fs.insert_tree(
5100            "/project",
5101            json!({
5102                ".git": {},
5103                "src": {
5104                    "main.rs": "fn main() {}"
5105                }
5106            }),
5107        )
5108        .await;
5109        fs.set_branch_name(Path::new("/project/.git"), Some("main"));
5110
5111        let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
5112
5113        let multi_workspace =
5114            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
5115
5116        let workspace = multi_workspace
5117            .read_with(cx, |multi_workspace, _cx| {
5118                multi_workspace.workspace().clone()
5119            })
5120            .unwrap();
5121
5122        workspace.update(cx, |workspace, _cx| {
5123            workspace.set_random_database_id();
5124        });
5125
5126        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
5127
5128        // Wait for the project to discover the git repository.
5129        cx.run_until_parked();
5130
5131        let panel = workspace.update_in(cx, |workspace, window, cx| {
5132            let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
5133            let panel =
5134                cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx));
5135            workspace.add_panel(panel.clone(), window, cx);
5136            panel
5137        });
5138
5139        cx.run_until_parked();
5140
5141        // Default thread target should be LocalProject.
5142        panel.read_with(cx, |panel, _cx| {
5143            assert_eq!(
5144                *panel.start_thread_in(),
5145                StartThreadIn::LocalProject,
5146                "default thread target should be LocalProject"
5147            );
5148        });
5149
5150        // Start a new thread with the default LocalProject target.
5151        // Use StubAgentServer so the thread connects immediately in tests.
5152        panel.update_in(cx, |panel, window, cx| {
5153            panel.open_external_thread_with_server(
5154                Rc::new(StubAgentServer::default_response()),
5155                window,
5156                cx,
5157            );
5158        });
5159
5160        cx.run_until_parked();
5161
5162        // MultiWorkspace should still have exactly one workspace (no worktree created).
5163        multi_workspace
5164            .read_with(cx, |multi_workspace, _cx| {
5165                assert_eq!(
5166                    multi_workspace.workspaces().len(),
5167                    1,
5168                    "LocalProject should not create a new workspace"
5169                );
5170            })
5171            .unwrap();
5172
5173        // The thread should be active in the panel.
5174        panel.read_with(cx, |panel, cx| {
5175            assert!(
5176                panel.active_agent_thread(cx).is_some(),
5177                "a thread should be running in the current workspace"
5178            );
5179        });
5180
5181        // The thread target should still be LocalProject (unchanged).
5182        panel.read_with(cx, |panel, _cx| {
5183            assert_eq!(
5184                *panel.start_thread_in(),
5185                StartThreadIn::LocalProject,
5186                "thread target should remain LocalProject"
5187            );
5188        });
5189
5190        // No worktree creation status should be set.
5191        panel.read_with(cx, |panel, _cx| {
5192            assert!(
5193                panel.worktree_creation_status.is_none(),
5194                "no worktree creation should have occurred"
5195            );
5196        });
5197    }
5198
5199    #[gpui::test]
5200    async fn test_thread_target_serialization_round_trip(cx: &mut TestAppContext) {
5201        init_test(cx);
5202        cx.update(|cx| {
5203            cx.update_flags(true, vec!["agent-v2".to_string()]);
5204            agent::ThreadStore::init_global(cx);
5205            language_model::LanguageModelRegistry::test(cx);
5206        });
5207
5208        let fs = FakeFs::new(cx.executor());
5209        fs.insert_tree(
5210            "/project",
5211            json!({
5212                ".git": {},
5213                "src": {
5214                    "main.rs": "fn main() {}"
5215                }
5216            }),
5217        )
5218        .await;
5219        fs.set_branch_name(Path::new("/project/.git"), Some("main"));
5220
5221        let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
5222
5223        let multi_workspace =
5224            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
5225
5226        let workspace = multi_workspace
5227            .read_with(cx, |multi_workspace, _cx| {
5228                multi_workspace.workspace().clone()
5229            })
5230            .unwrap();
5231
5232        workspace.update(cx, |workspace, _cx| {
5233            workspace.set_random_database_id();
5234        });
5235
5236        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
5237
5238        // Wait for the project to discover the git repository.
5239        cx.run_until_parked();
5240
5241        let panel = workspace.update_in(cx, |workspace, window, cx| {
5242            let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
5243            let panel =
5244                cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx));
5245            workspace.add_panel(panel.clone(), window, cx);
5246            panel
5247        });
5248
5249        cx.run_until_parked();
5250
5251        // Default should be LocalProject.
5252        panel.read_with(cx, |panel, _cx| {
5253            assert_eq!(*panel.start_thread_in(), StartThreadIn::LocalProject);
5254        });
5255
5256        // Change thread target to NewWorktree.
5257        panel.update(cx, |panel, cx| {
5258            panel.set_start_thread_in(&StartThreadIn::NewWorktree, cx);
5259        });
5260
5261        panel.read_with(cx, |panel, _cx| {
5262            assert_eq!(
5263                *panel.start_thread_in(),
5264                StartThreadIn::NewWorktree,
5265                "thread target should be NewWorktree after set_thread_target"
5266            );
5267        });
5268
5269        // Let serialization complete.
5270        cx.run_until_parked();
5271
5272        // Load a fresh panel from the serialized data.
5273        let prompt_builder = Arc::new(prompt_store::PromptBuilder::new(None).unwrap());
5274        let async_cx = cx.update(|window, cx| window.to_async(cx));
5275        let loaded_panel =
5276            AgentPanel::load(workspace.downgrade(), prompt_builder.clone(), async_cx)
5277                .await
5278                .expect("panel load should succeed");
5279        cx.run_until_parked();
5280
5281        loaded_panel.read_with(cx, |panel, _cx| {
5282            assert_eq!(
5283                *panel.start_thread_in(),
5284                StartThreadIn::NewWorktree,
5285                "thread target should survive serialization round-trip"
5286            );
5287        });
5288    }
5289
5290    #[gpui::test]
5291    async fn test_set_active_blocked_during_worktree_creation(cx: &mut TestAppContext) {
5292        init_test(cx);
5293
5294        let fs = FakeFs::new(cx.executor());
5295        cx.update(|cx| {
5296            cx.update_flags(true, vec!["agent-v2".to_string()]);
5297            agent::ThreadStore::init_global(cx);
5298            language_model::LanguageModelRegistry::test(cx);
5299            <dyn fs::Fs>::set_global(fs.clone(), cx);
5300        });
5301
5302        fs.insert_tree(
5303            "/project",
5304            json!({
5305                ".git": {},
5306                "src": {
5307                    "main.rs": "fn main() {}"
5308                }
5309            }),
5310        )
5311        .await;
5312
5313        let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
5314
5315        let multi_workspace =
5316            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
5317
5318        let workspace = multi_workspace
5319            .read_with(cx, |multi_workspace, _cx| {
5320                multi_workspace.workspace().clone()
5321            })
5322            .unwrap();
5323
5324        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
5325
5326        let panel = workspace.update_in(cx, |workspace, window, cx| {
5327            let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
5328            let panel =
5329                cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx));
5330            workspace.add_panel(panel.clone(), window, cx);
5331            panel
5332        });
5333
5334        cx.run_until_parked();
5335
5336        // Simulate worktree creation in progress and reset to Uninitialized
5337        panel.update_in(cx, |panel, window, cx| {
5338            panel.worktree_creation_status = Some(WorktreeCreationStatus::Creating);
5339            panel.active_view = ActiveView::Uninitialized;
5340            Panel::set_active(panel, true, window, cx);
5341            assert!(
5342                matches!(panel.active_view, ActiveView::Uninitialized),
5343                "set_active should not create a thread while worktree is being created"
5344            );
5345        });
5346
5347        // Clear the creation status and use open_external_thread_with_server
5348        // (which bypasses new_agent_thread) to verify the panel can transition
5349        // out of Uninitialized. We can't call set_active directly because
5350        // new_agent_thread requires full agent server infrastructure.
5351        panel.update_in(cx, |panel, window, cx| {
5352            panel.worktree_creation_status = None;
5353            panel.active_view = ActiveView::Uninitialized;
5354            panel.open_external_thread_with_server(
5355                Rc::new(StubAgentServer::default_response()),
5356                window,
5357                cx,
5358            );
5359        });
5360
5361        cx.run_until_parked();
5362
5363        panel.read_with(cx, |panel, _cx| {
5364            assert!(
5365                !matches!(panel.active_view, ActiveView::Uninitialized),
5366                "panel should transition out of Uninitialized once worktree creation is cleared"
5367            );
5368        });
5369    }
5370
5371    #[test]
5372    fn test_deserialize_legacy_agent_type_variants() {
5373        assert_eq!(
5374            serde_json::from_str::<AgentType>(r#""ClaudeAgent""#).unwrap(),
5375            AgentType::Custom {
5376                name: CLAUDE_AGENT_NAME.into(),
5377            },
5378        );
5379        assert_eq!(
5380            serde_json::from_str::<AgentType>(r#""ClaudeCode""#).unwrap(),
5381            AgentType::Custom {
5382                name: CLAUDE_AGENT_NAME.into(),
5383            },
5384        );
5385        assert_eq!(
5386            serde_json::from_str::<AgentType>(r#""Codex""#).unwrap(),
5387            AgentType::Custom {
5388                name: CODEX_NAME.into(),
5389            },
5390        );
5391        assert_eq!(
5392            serde_json::from_str::<AgentType>(r#""Gemini""#).unwrap(),
5393            AgentType::Custom {
5394                name: GEMINI_NAME.into(),
5395            },
5396        );
5397    }
5398
5399    #[test]
5400    fn test_deserialize_current_agent_type_variants() {
5401        assert_eq!(
5402            serde_json::from_str::<AgentType>(r#""NativeAgent""#).unwrap(),
5403            AgentType::NativeAgent,
5404        );
5405        assert_eq!(
5406            serde_json::from_str::<AgentType>(r#""TextThread""#).unwrap(),
5407            AgentType::TextThread,
5408        );
5409        assert_eq!(
5410            serde_json::from_str::<AgentType>(r#"{"Custom":{"name":"my-agent"}}"#).unwrap(),
5411            AgentType::Custom {
5412                name: "my-agent".into(),
5413            },
5414        );
5415    }
5416
5417    #[test]
5418    fn test_deserialize_legacy_serialized_panel() {
5419        let json = serde_json::json!({
5420            "width": 300.0,
5421            "selected_agent": "ClaudeAgent",
5422            "last_active_thread": {
5423                "session_id": "test-session",
5424                "agent_type": "Codex",
5425            },
5426        });
5427
5428        let panel: SerializedAgentPanel = serde_json::from_value(json).unwrap();
5429        assert_eq!(
5430            panel.selected_agent,
5431            Some(AgentType::Custom {
5432                name: CLAUDE_AGENT_NAME.into(),
5433            }),
5434        );
5435        let thread = panel.last_active_thread.unwrap();
5436        assert_eq!(
5437            thread.agent_type,
5438            AgentType::Custom {
5439                name: CODEX_NAME.into(),
5440            },
5441        );
5442    }
5443}