thread_view.rs

   1use acp_thread::{
   2    AcpThread, AcpThreadEvent, AgentSessionInfo, AgentThreadEntry, AssistantMessage,
   3    AssistantMessageChunk, AuthRequired, LoadError, MentionUri, PermissionOptionChoice,
   4    PermissionOptions, RetryStatus, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus,
   5    UserMessageId,
   6};
   7use acp_thread::{AgentConnection, Plan};
   8use action_log::{ActionLog, ActionLogTelemetry};
   9use agent::{NativeAgentServer, NativeAgentSessionList, SharedThread, ThreadStore};
  10use agent_client_protocol::{self as acp, PromptCapabilities};
  11use agent_servers::{AgentServer, AgentServerDelegate};
  12use agent_settings::{AgentProfileId, AgentSettings};
  13use anyhow::{Result, anyhow};
  14use arrayvec::ArrayVec;
  15use audio::{Audio, Sound};
  16use buffer_diff::BufferDiff;
  17use client::zed_urls;
  18use collections::{HashMap, HashSet};
  19use editor::scroll::Autoscroll;
  20use editor::{
  21    Editor, EditorEvent, EditorMode, MultiBuffer, PathKey, SelectionEffects, SizingBehavior,
  22};
  23use feature_flags::{
  24    AgentSharingFeatureFlag, AgentV2FeatureFlag, CloudThinkingEffortFeatureFlag,
  25    FeatureFlagAppExt as _,
  26};
  27use file_icons::FileIcons;
  28use fs::Fs;
  29use futures::FutureExt as _;
  30use gpui::{
  31    Action, Animation, AnimationExt, AnyView, App, ClickEvent, ClipboardItem, CursorStyle,
  32    ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, ListOffset, ListState, ObjectFit,
  33    PlatformDisplay, ScrollHandle, SharedString, Subscription, Task, TextStyle, WeakEntity, Window,
  34    WindowHandle, div, ease_in_out, img, linear_color_stop, linear_gradient, list, point,
  35    pulsating_between,
  36};
  37use language::Buffer;
  38use language_model::LanguageModelRegistry;
  39use markdown::{Markdown, MarkdownElement, MarkdownFont, MarkdownStyle};
  40use project::{AgentServerStore, ExternalAgentServerName, Project, ProjectEntryId};
  41use prompt_store::{PromptId, PromptStore};
  42use rope::Point;
  43use settings::{NotifyWhenAgentWaiting, Settings as _, SettingsStore};
  44use std::cell::RefCell;
  45use std::path::Path;
  46use std::sync::Arc;
  47use std::time::Instant;
  48use std::{collections::BTreeMap, rc::Rc, time::Duration};
  49use terminal_view::terminal_panel::TerminalPanel;
  50use text::{Anchor, ToPoint as _};
  51use theme::AgentFontSize;
  52use ui::{
  53    Callout, CommonAnimationExt, ContextMenu, ContextMenuEntry, CopyButton, DecoratedIcon,
  54    DiffStat, Disclosure, Divider, DividerColor, IconDecoration, IconDecorationKind, KeyBinding,
  55    PopoverMenu, PopoverMenuHandle, SpinnerLabel, TintColor, Tooltip, WithScrollbar, prelude::*,
  56    right_click_menu,
  57};
  58use util::defer;
  59use util::{ResultExt, size::format_file_size, time::duration_alt_display};
  60use workspace::{
  61    CollaboratorId, MultiWorkspace, NewTerminal, Toast, Workspace, notifications::NotificationId,
  62};
  63use zed_actions::agent::{Chat, ToggleModelSelector};
  64use zed_actions::assistant::OpenRulesLibrary;
  65
  66use super::config_options::ConfigOptionsView;
  67use super::entry_view_state::EntryViewState;
  68use super::thread_history::AcpThreadHistory;
  69use crate::acp::AcpModelSelectorPopover;
  70use crate::acp::ModeSelector;
  71use crate::acp::entry_view_state::{EntryViewEvent, ViewEvent};
  72use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
  73use crate::agent_diff::AgentDiff;
  74use crate::profile_selector::{ProfileProvider, ProfileSelector};
  75use crate::ui::{AgentNotification, AgentNotificationEvent};
  76use crate::{
  77    AgentDiffPane, AgentPanel, AllowAlways, AllowOnce, AuthorizeToolCall, ClearMessageQueue,
  78    CycleFavoriteModels, CycleModeSelector, CycleThinkingEffort, EditFirstQueuedMessage,
  79    ExpandMessageEditor, ExternalAgentInitialContent, Follow, KeepAll, NewThread,
  80    OpenAddContextMenu, OpenAgentDiff, OpenHistory, RejectAll, RejectOnce,
  81    RemoveFirstQueuedMessage, SelectPermissionGranularity, SendImmediately, SendNextQueuedMessage,
  82    ToggleProfileSelector, ToggleThinkingEffortMenu, ToggleThinkingMode,
  83};
  84
  85const STOPWATCH_THRESHOLD: Duration = Duration::from_secs(30);
  86const TOKEN_THRESHOLD: u64 = 250;
  87
  88mod active_thread;
  89pub use active_thread::*;
  90
  91pub struct QueuedMessage {
  92    pub content: Vec<acp::ContentBlock>,
  93    pub tracked_buffers: Vec<Entity<Buffer>>,
  94}
  95
  96#[derive(Copy, Clone, Debug, PartialEq, Eq)]
  97enum ThreadFeedback {
  98    Positive,
  99    Negative,
 100}
 101
 102#[derive(Debug)]
 103pub(crate) enum ThreadError {
 104    PaymentRequired,
 105    Refusal,
 106    AuthenticationRequired(SharedString),
 107    Other {
 108        message: SharedString,
 109        acp_error_code: Option<SharedString>,
 110    },
 111}
 112
 113impl ThreadError {
 114    fn from_err(error: anyhow::Error, agent_name: &str) -> Self {
 115        if error.is::<language_model::PaymentRequiredError>() {
 116            Self::PaymentRequired
 117        } else if let Some(acp_error) = error.downcast_ref::<acp::Error>()
 118            && acp_error.code == acp::ErrorCode::AuthRequired
 119        {
 120            Self::AuthenticationRequired(acp_error.message.clone().into())
 121        } else {
 122            let message: SharedString = format!("{:#}", error).into();
 123
 124            // Extract ACP error code if available
 125            let acp_error_code = error
 126                .downcast_ref::<acp::Error>()
 127                .map(|acp_error| SharedString::from(acp_error.code.to_string()));
 128
 129            // TODO: we should have Gemini return better errors here.
 130            if agent_name == "Gemini CLI"
 131                && message.contains("Could not load the default credentials")
 132                || message.contains("API key not valid")
 133                || message.contains("Request had invalid authentication credentials")
 134            {
 135                Self::AuthenticationRequired(message)
 136            } else {
 137                Self::Other {
 138                    message,
 139                    acp_error_code,
 140                }
 141            }
 142        }
 143    }
 144}
 145
 146impl ProfileProvider for Entity<agent::Thread> {
 147    fn profile_id(&self, cx: &App) -> AgentProfileId {
 148        self.read(cx).profile().clone()
 149    }
 150
 151    fn set_profile(&self, profile_id: AgentProfileId, cx: &mut App) {
 152        self.update(cx, |thread, cx| {
 153            // Apply the profile and let the thread swap to its default model.
 154            thread.set_profile(profile_id, cx);
 155        });
 156    }
 157
 158    fn profiles_supported(&self, cx: &App) -> bool {
 159        self.read(cx)
 160            .model()
 161            .is_some_and(|model| model.supports_tools())
 162    }
 163}
 164
 165pub struct AcpServerView {
 166    agent: Rc<dyn AgentServer>,
 167    agent_server_store: Entity<AgentServerStore>,
 168    workspace: WeakEntity<Workspace>,
 169    project: Entity<Project>,
 170    thread_store: Option<Entity<ThreadStore>>,
 171    prompt_store: Option<Entity<PromptStore>>,
 172    server_state: ServerState,
 173    login: Option<task::SpawnInTerminal>, // is some <=> Active | Unauthenticated
 174    history: Entity<AcpThreadHistory>,
 175    focus_handle: FocusHandle,
 176    notifications: Vec<WindowHandle<AgentNotification>>,
 177    notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
 178    auth_task: Option<Task<()>>,
 179    _subscriptions: Vec<Subscription>,
 180}
 181
 182impl AcpServerView {
 183    pub fn active_thread(&self) -> Option<Entity<AcpThreadView>> {
 184        match &self.server_state {
 185            ServerState::Connected(connected) => Some(connected.current.clone()),
 186            _ => None,
 187        }
 188    }
 189
 190    pub fn parent_thread(&self, cx: &App) -> Option<Entity<AcpThreadView>> {
 191        match &self.server_state {
 192            ServerState::Connected(connected) => {
 193                let mut current = connected.current.clone();
 194                while let Some(parent_id) = current.read(cx).parent_id.clone() {
 195                    if let Some(parent) = connected.threads.get(&parent_id) {
 196                        current = parent.clone();
 197                    } else {
 198                        break;
 199                    }
 200                }
 201                Some(current)
 202            }
 203            _ => None,
 204        }
 205    }
 206
 207    pub fn thread_view(&self, session_id: &acp::SessionId) -> Option<Entity<AcpThreadView>> {
 208        let connected = self.as_connected()?;
 209        connected.threads.get(session_id).cloned()
 210    }
 211
 212    pub fn as_connected(&self) -> Option<&ConnectedServerState> {
 213        match &self.server_state {
 214            ServerState::Connected(connected) => Some(connected),
 215            _ => None,
 216        }
 217    }
 218
 219    pub fn as_connected_mut(&mut self) -> Option<&mut ConnectedServerState> {
 220        match &mut self.server_state {
 221            ServerState::Connected(connected) => Some(connected),
 222            _ => None,
 223        }
 224    }
 225
 226    pub fn navigate_to_session(
 227        &mut self,
 228        session_id: acp::SessionId,
 229        window: &mut Window,
 230        cx: &mut Context<Self>,
 231    ) {
 232        let Some(connected) = self.as_connected_mut() else {
 233            return;
 234        };
 235
 236        connected.navigate_to_session(session_id);
 237        if let Some(view) = self.active_thread() {
 238            view.focus_handle(cx).focus(window, cx);
 239        }
 240        cx.notify();
 241    }
 242}
 243
 244enum ServerState {
 245    Loading(Entity<LoadingView>),
 246    LoadError(LoadError),
 247    Connected(ConnectedServerState),
 248}
 249
 250// current -> Entity
 251// hashmap of threads, current becomes session_id
 252pub struct ConnectedServerState {
 253    auth_state: AuthState,
 254    current: Entity<AcpThreadView>,
 255    threads: HashMap<acp::SessionId, Entity<AcpThreadView>>,
 256    connection: Rc<dyn AgentConnection>,
 257}
 258
 259enum AuthState {
 260    Ok,
 261    Unauthenticated {
 262        description: Option<Entity<Markdown>>,
 263        configuration_view: Option<AnyView>,
 264        pending_auth_method: Option<acp::AuthMethodId>,
 265        _subscription: Option<Subscription>,
 266    },
 267}
 268
 269impl AuthState {
 270    pub fn is_ok(&self) -> bool {
 271        matches!(self, Self::Ok)
 272    }
 273}
 274
 275struct LoadingView {
 276    title: SharedString,
 277    _load_task: Task<()>,
 278    _update_title_task: Task<anyhow::Result<()>>,
 279}
 280
 281impl ConnectedServerState {
 282    pub fn has_thread_error(&self, cx: &App) -> bool {
 283        self.current.read(cx).thread_error.is_some()
 284    }
 285
 286    pub fn navigate_to_session(&mut self, session_id: acp::SessionId) {
 287        if let Some(session) = self.threads.get(&session_id) {
 288            self.current = session.clone();
 289        }
 290    }
 291
 292    pub fn close_all_sessions(&self, cx: &mut App) -> Task<()> {
 293        let tasks = self
 294            .threads
 295            .keys()
 296            .map(|id| self.connection.close_session(id, cx));
 297        let task = futures::future::join_all(tasks);
 298        cx.background_spawn(async move {
 299            task.await;
 300        })
 301    }
 302}
 303
 304impl AcpServerView {
 305    pub fn new(
 306        agent: Rc<dyn AgentServer>,
 307        resume_thread: Option<AgentSessionInfo>,
 308        initial_content: Option<ExternalAgentInitialContent>,
 309        workspace: WeakEntity<Workspace>,
 310        project: Entity<Project>,
 311        thread_store: Option<Entity<ThreadStore>>,
 312        prompt_store: Option<Entity<PromptStore>>,
 313        history: Entity<AcpThreadHistory>,
 314        window: &mut Window,
 315        cx: &mut Context<Self>,
 316    ) -> Self {
 317        let agent_server_store = project.read(cx).agent_server_store().clone();
 318        let subscriptions = vec![
 319            cx.observe_global_in::<SettingsStore>(window, Self::agent_ui_font_size_changed),
 320            cx.observe_global_in::<AgentFontSize>(window, Self::agent_ui_font_size_changed),
 321            cx.subscribe_in(
 322                &agent_server_store,
 323                window,
 324                Self::handle_agent_servers_updated,
 325            ),
 326        ];
 327
 328        cx.on_release(|this, cx| {
 329            if let Some(connected) = this.as_connected() {
 330                connected.close_all_sessions(cx).detach();
 331            }
 332            for window in this.notifications.drain(..) {
 333                window
 334                    .update(cx, |_, window, _| {
 335                        window.remove_window();
 336                    })
 337                    .ok();
 338            }
 339        })
 340        .detach();
 341
 342        Self {
 343            agent: agent.clone(),
 344            agent_server_store,
 345            workspace,
 346            project: project.clone(),
 347            thread_store,
 348            prompt_store,
 349            server_state: Self::initial_state(
 350                agent.clone(),
 351                resume_thread,
 352                project,
 353                initial_content,
 354                window,
 355                cx,
 356            ),
 357            login: None,
 358            notifications: Vec::new(),
 359            notification_subscriptions: HashMap::default(),
 360            auth_task: None,
 361            history,
 362            _subscriptions: subscriptions,
 363            focus_handle: cx.focus_handle(),
 364        }
 365    }
 366
 367    fn set_server_state(&mut self, state: ServerState, cx: &mut Context<Self>) {
 368        if let Some(connected) = self.as_connected() {
 369            connected.close_all_sessions(cx).detach();
 370        }
 371
 372        self.server_state = state;
 373        cx.notify();
 374    }
 375
 376    fn reset(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 377        let resume_thread_metadata = self
 378            .active_thread()
 379            .and_then(|thread| thread.read(cx).resume_thread_metadata.clone());
 380
 381        let state = Self::initial_state(
 382            self.agent.clone(),
 383            resume_thread_metadata,
 384            self.project.clone(),
 385            None,
 386            window,
 387            cx,
 388        );
 389        self.set_server_state(state, cx);
 390
 391        if let Some(connected) = self.as_connected() {
 392            connected.current.update(cx, |this, cx| {
 393                this.message_editor.update(cx, |editor, cx| {
 394                    editor.set_command_state(
 395                        this.prompt_capabilities.clone(),
 396                        this.available_commands.clone(),
 397                        cx,
 398                    );
 399                });
 400            });
 401        }
 402        cx.notify();
 403    }
 404
 405    fn initial_state(
 406        agent: Rc<dyn AgentServer>,
 407        resume_thread: Option<AgentSessionInfo>,
 408        project: Entity<Project>,
 409        initial_content: Option<ExternalAgentInitialContent>,
 410        window: &mut Window,
 411        cx: &mut Context<Self>,
 412    ) -> ServerState {
 413        if project.read(cx).is_via_collab()
 414            && agent.clone().downcast::<NativeAgentServer>().is_none()
 415        {
 416            return ServerState::LoadError(LoadError::Other(
 417                "External agents are not yet supported in shared projects.".into(),
 418            ));
 419        }
 420        let mut worktrees = project.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
 421        // Pick the first non-single-file worktree for the root directory if there are any,
 422        // and otherwise the parent of a single-file worktree, falling back to $HOME if there are no visible worktrees.
 423        worktrees.sort_by(|l, r| {
 424            l.read(cx)
 425                .is_single_file()
 426                .cmp(&r.read(cx).is_single_file())
 427        });
 428        let worktree_roots: Vec<Arc<Path>> = worktrees
 429            .iter()
 430            .filter_map(|worktree| {
 431                let worktree = worktree.read(cx);
 432                if worktree.is_single_file() {
 433                    Some(worktree.abs_path().parent()?.into())
 434                } else {
 435                    Some(worktree.abs_path())
 436                }
 437            })
 438            .collect();
 439        let root_dir = worktree_roots.first().cloned();
 440        let session_cwd = resume_thread
 441            .as_ref()
 442            .and_then(|resume| {
 443                resume
 444                    .cwd
 445                    .as_ref()
 446                    .and_then(|cwd| util::paths::normalize_lexically(cwd).ok())
 447                    .filter(|cwd| {
 448                        worktree_roots
 449                            .iter()
 450                            .any(|root| cwd.starts_with(root.as_ref()))
 451                    })
 452                    .map(|path| path.into())
 453            })
 454            .or_else(|| root_dir.clone())
 455            .unwrap_or_else(|| paths::home_dir().as_path().into());
 456
 457        let (status_tx, mut status_rx) = watch::channel("Loading…".into());
 458        let (new_version_available_tx, mut new_version_available_rx) = watch::channel(None);
 459        let delegate = AgentServerDelegate::new(
 460            project.read(cx).agent_server_store().clone(),
 461            project.clone(),
 462            Some(status_tx),
 463            Some(new_version_available_tx),
 464        );
 465
 466        let connect_task = agent.connect(root_dir.as_deref(), delegate, cx);
 467        let load_task = cx.spawn_in(window, async move |this, cx| {
 468            let connection = match connect_task.await {
 469                Ok((connection, login)) => {
 470                    this.update(cx, |this, _| this.login = login).ok();
 471                    connection
 472                }
 473                Err(err) => {
 474                    this.update_in(cx, |this, window, cx| {
 475                        if err.downcast_ref::<LoadError>().is_some() {
 476                            this.handle_load_error(err, window, cx);
 477                        } else if let Some(active) = this.active_thread() {
 478                            active.update(cx, |active, cx| active.handle_any_thread_error(err, cx));
 479                        }
 480                        cx.notify();
 481                    })
 482                    .log_err();
 483                    return;
 484                }
 485            };
 486
 487            telemetry::event!("Agent Thread Started", agent = connection.telemetry_id());
 488
 489            let mut resumed_without_history = false;
 490            let result = if let Some(resume) = resume_thread.clone() {
 491                cx.update(|_, cx| {
 492                    if connection.supports_load_session(cx) {
 493                        connection
 494                            .clone()
 495                            .load_session(resume, project.clone(), &session_cwd, cx)
 496                    } else if connection.supports_resume_session(cx) {
 497                        resumed_without_history = true;
 498                        connection
 499                            .clone()
 500                            .resume_session(resume, project.clone(), &session_cwd, cx)
 501                    } else {
 502                        Task::ready(Err(anyhow!(LoadError::Other(
 503                            "Loading or resuming sessions is not supported by this agent.".into()
 504                        ))))
 505                    }
 506                })
 507                .log_err()
 508            } else {
 509                cx.update(|_, cx| {
 510                    connection
 511                        .clone()
 512                        .new_session(project.clone(), session_cwd.as_ref(), cx)
 513                })
 514                .log_err()
 515            };
 516
 517            let Some(result) = result else {
 518                return;
 519            };
 520
 521            let result = match result.await {
 522                Err(e) => match e.downcast::<acp_thread::AuthRequired>() {
 523                    Ok(err) => {
 524                        cx.update(|window, cx| {
 525                            Self::handle_auth_required(this, err, agent.name(), window, cx)
 526                        })
 527                        .log_err();
 528                        return;
 529                    }
 530                    Err(err) => Err(err),
 531                },
 532                Ok(thread) => Ok(thread),
 533            };
 534
 535            this.update_in(cx, |this, window, cx| {
 536                match result {
 537                    Ok(thread) => {
 538                        let current = this.new_thread_view(
 539                            None,
 540                            thread,
 541                            resumed_without_history,
 542                            resume_thread,
 543                            initial_content,
 544                            window,
 545                            cx,
 546                        );
 547
 548                        if this.focus_handle.contains_focused(window, cx) {
 549                            current
 550                                .read(cx)
 551                                .message_editor
 552                                .focus_handle(cx)
 553                                .focus(window, cx);
 554                        }
 555
 556                        this.set_server_state(
 557                            ServerState::Connected(ConnectedServerState {
 558                                connection,
 559                                auth_state: AuthState::Ok,
 560                                current: current.clone(),
 561                                threads: HashMap::from_iter([(
 562                                    current.read(cx).thread.read(cx).session_id().clone(),
 563                                    current,
 564                                )]),
 565                            }),
 566                            cx,
 567                        );
 568                    }
 569                    Err(err) => {
 570                        this.handle_load_error(err, window, cx);
 571                    }
 572                };
 573            })
 574            .log_err();
 575        });
 576
 577        cx.spawn(async move |this, cx| {
 578            while let Ok(new_version) = new_version_available_rx.recv().await {
 579                if let Some(new_version) = new_version {
 580                    this.update(cx, |this, cx| {
 581                        if let Some(thread) = this.active_thread() {
 582                            thread.update(cx, |thread, _cx| {
 583                                thread.new_server_version_available = Some(new_version.into());
 584                            });
 585                        }
 586                        cx.notify();
 587                    })
 588                    .ok();
 589                }
 590            }
 591        })
 592        .detach();
 593
 594        let loading_view = cx.new(|cx| {
 595            let update_title_task = cx.spawn(async move |this, cx| {
 596                loop {
 597                    let status = status_rx.recv().await?;
 598                    this.update(cx, |this: &mut LoadingView, cx| {
 599                        this.title = status;
 600                        cx.notify();
 601                    })?;
 602                }
 603            });
 604
 605            LoadingView {
 606                title: "Loading…".into(),
 607                _load_task: load_task,
 608                _update_title_task: update_title_task,
 609            }
 610        });
 611
 612        ServerState::Loading(loading_view)
 613    }
 614
 615    fn new_thread_view(
 616        &self,
 617        parent_id: Option<acp::SessionId>,
 618        thread: Entity<AcpThread>,
 619        resumed_without_history: bool,
 620        resume_thread: Option<AgentSessionInfo>,
 621        initial_content: Option<ExternalAgentInitialContent>,
 622        window: &mut Window,
 623        cx: &mut Context<Self>,
 624    ) -> Entity<AcpThreadView> {
 625        let agent_name = self.agent.name();
 626        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
 627        let available_commands = Rc::new(RefCell::new(vec![]));
 628
 629        let action_log = thread.read(cx).action_log().clone();
 630
 631        prompt_capabilities.replace(thread.read(cx).prompt_capabilities());
 632
 633        let entry_view_state = cx.new(|_| {
 634            EntryViewState::new(
 635                self.workspace.clone(),
 636                self.project.downgrade(),
 637                self.thread_store.clone(),
 638                self.history.downgrade(),
 639                self.prompt_store.clone(),
 640                prompt_capabilities.clone(),
 641                available_commands.clone(),
 642                self.agent.name(),
 643            )
 644        });
 645
 646        let count = thread.read(cx).entries().len();
 647        let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0));
 648        entry_view_state.update(cx, |view_state, cx| {
 649            for ix in 0..count {
 650                view_state.sync_entry(ix, &thread, window, cx);
 651            }
 652            list_state.splice_focusable(
 653                0..0,
 654                (0..count).map(|ix| view_state.entry(ix)?.focus_handle(cx)),
 655            );
 656        });
 657
 658        AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx);
 659
 660        let connection = thread.read(cx).connection().clone();
 661        let session_id = thread.read(cx).session_id().clone();
 662        let session_list = if connection.supports_session_history(cx) {
 663            connection.session_list(cx)
 664        } else {
 665            None
 666        };
 667        self.history.update(cx, |history, cx| {
 668            history.set_session_list(session_list, cx);
 669        });
 670
 671        // Check for config options first
 672        // Config options take precedence over legacy mode/model selectors
 673        // (feature flag gating happens at the data layer)
 674        let config_options_provider = connection.session_config_options(&session_id, cx);
 675
 676        let config_options_view;
 677        let mode_selector;
 678        let model_selector;
 679        if let Some(config_options) = config_options_provider {
 680            // Use config options - don't create mode_selector or model_selector
 681            let agent_server = self.agent.clone();
 682            let fs = self.project.read(cx).fs().clone();
 683            config_options_view =
 684                Some(cx.new(|cx| {
 685                    ConfigOptionsView::new(config_options, agent_server, fs, window, cx)
 686                }));
 687            model_selector = None;
 688            mode_selector = None;
 689        } else {
 690            // Fall back to legacy mode/model selectors
 691            config_options_view = None;
 692            model_selector = connection.model_selector(&session_id).map(|selector| {
 693                let agent_server = self.agent.clone();
 694                let fs = self.project.read(cx).fs().clone();
 695                cx.new(|cx| {
 696                    AcpModelSelectorPopover::new(
 697                        selector,
 698                        agent_server,
 699                        fs,
 700                        PopoverMenuHandle::default(),
 701                        self.focus_handle(cx),
 702                        window,
 703                        cx,
 704                    )
 705                })
 706            });
 707
 708            mode_selector = connection
 709                .session_modes(&session_id, cx)
 710                .map(|session_modes| {
 711                    let fs = self.project.read(cx).fs().clone();
 712                    cx.new(|_cx| ModeSelector::new(session_modes, self.agent.clone(), fs))
 713                });
 714        }
 715
 716        let mut subscriptions = vec![
 717            cx.subscribe_in(&thread, window, Self::handle_thread_event),
 718            cx.observe(&action_log, |_, _, cx| cx.notify()),
 719        ];
 720
 721        let parent_session_id = thread.read(cx).session_id().clone();
 722        let subagent_sessions = thread
 723            .read(cx)
 724            .entries()
 725            .iter()
 726            .filter_map(|entry| match entry {
 727                AgentThreadEntry::ToolCall(call) => call.subagent_session_id.clone(),
 728                _ => None,
 729            })
 730            .collect::<Vec<_>>();
 731
 732        if !subagent_sessions.is_empty() {
 733            cx.spawn_in(window, async move |this, cx| {
 734                this.update_in(cx, |this, window, cx| {
 735                    for subagent_id in subagent_sessions {
 736                        this.load_subagent_session(
 737                            subagent_id,
 738                            parent_session_id.clone(),
 739                            window,
 740                            cx,
 741                        );
 742                    }
 743                })
 744            })
 745            .detach();
 746        }
 747
 748        let title_editor = if thread.update(cx, |thread, cx| thread.can_set_title(cx)) {
 749            let editor = cx.new(|cx| {
 750                let mut editor = Editor::single_line(window, cx);
 751                editor.set_text(thread.read(cx).title(), window, cx);
 752                editor
 753            });
 754            subscriptions.push(cx.subscribe_in(&editor, window, Self::handle_title_editor_event));
 755            Some(editor)
 756        } else {
 757            None
 758        };
 759
 760        let profile_selector: Option<Rc<agent::NativeAgentConnection>> =
 761            connection.clone().downcast();
 762        let profile_selector = profile_selector
 763            .and_then(|native_connection| native_connection.thread(&session_id, cx))
 764            .map(|native_thread| {
 765                cx.new(|cx| {
 766                    ProfileSelector::new(
 767                        <dyn Fs>::global(cx),
 768                        Arc::new(native_thread),
 769                        self.focus_handle(cx),
 770                        cx,
 771                    )
 772                })
 773            });
 774
 775        let agent_display_name = self
 776            .agent_server_store
 777            .read(cx)
 778            .agent_display_name(&ExternalAgentServerName(agent_name.clone()))
 779            .unwrap_or_else(|| agent_name.clone());
 780
 781        let agent_icon = self.agent.logo();
 782
 783        let weak = cx.weak_entity();
 784        cx.new(|cx| {
 785            AcpThreadView::new(
 786                parent_id,
 787                thread,
 788                self.login.clone(),
 789                weak,
 790                agent_icon,
 791                agent_name,
 792                agent_display_name,
 793                self.workspace.clone(),
 794                entry_view_state,
 795                title_editor,
 796                config_options_view,
 797                mode_selector,
 798                model_selector,
 799                profile_selector,
 800                list_state,
 801                prompt_capabilities,
 802                available_commands,
 803                resumed_without_history,
 804                resume_thread,
 805                self.project.downgrade(),
 806                self.thread_store.clone(),
 807                self.history.clone(),
 808                self.prompt_store.clone(),
 809                initial_content,
 810                subscriptions,
 811                window,
 812                cx,
 813            )
 814        })
 815    }
 816
 817    fn handle_auth_required(
 818        this: WeakEntity<Self>,
 819        err: AuthRequired,
 820        agent_name: SharedString,
 821        window: &mut Window,
 822        cx: &mut App,
 823    ) {
 824        let (configuration_view, subscription) = if let Some(provider_id) = &err.provider_id {
 825            let registry = LanguageModelRegistry::global(cx);
 826
 827            let sub = window.subscribe(&registry, cx, {
 828                let provider_id = provider_id.clone();
 829                let this = this.clone();
 830                move |_, ev, window, cx| {
 831                    if let language_model::Event::ProviderStateChanged(updated_provider_id) = &ev
 832                        && &provider_id == updated_provider_id
 833                        && LanguageModelRegistry::global(cx)
 834                            .read(cx)
 835                            .provider(&provider_id)
 836                            .map_or(false, |provider| provider.is_authenticated(cx))
 837                    {
 838                        this.update(cx, |this, cx| {
 839                            this.reset(window, cx);
 840                        })
 841                        .ok();
 842                    }
 843                }
 844            });
 845
 846            let view = registry.read(cx).provider(&provider_id).map(|provider| {
 847                provider.configuration_view(
 848                    language_model::ConfigurationViewTargetAgent::Other(agent_name),
 849                    window,
 850                    cx,
 851                )
 852            });
 853
 854            (view, Some(sub))
 855        } else {
 856            (None, None)
 857        };
 858
 859        this.update(cx, |this, cx| {
 860            if let Some(connected) = this.as_connected_mut() {
 861                let description = err
 862                    .description
 863                    .map(|desc| cx.new(|cx| Markdown::new(desc.into(), None, None, cx)));
 864
 865                connected.auth_state = AuthState::Unauthenticated {
 866                    pending_auth_method: None,
 867                    configuration_view,
 868                    description,
 869                    _subscription: subscription,
 870                };
 871                if connected
 872                    .current
 873                    .read(cx)
 874                    .message_editor
 875                    .focus_handle(cx)
 876                    .is_focused(window)
 877                {
 878                    this.focus_handle.focus(window, cx)
 879                }
 880            }
 881            cx.notify();
 882        })
 883        .ok();
 884    }
 885
 886    fn handle_load_error(
 887        &mut self,
 888        err: anyhow::Error,
 889        window: &mut Window,
 890        cx: &mut Context<Self>,
 891    ) {
 892        match &self.server_state {
 893            ServerState::Connected(connected) => {
 894                if connected
 895                    .current
 896                    .read(cx)
 897                    .message_editor
 898                    .focus_handle(cx)
 899                    .is_focused(window)
 900                {
 901                    self.focus_handle.focus(window, cx)
 902                }
 903            }
 904            _ => {}
 905        }
 906        let load_error = if let Some(load_err) = err.downcast_ref::<LoadError>() {
 907            load_err.clone()
 908        } else {
 909            LoadError::Other(format!("{:#}", err).into())
 910        };
 911        self.emit_load_error_telemetry(&load_error);
 912        self.set_server_state(ServerState::LoadError(load_error), cx);
 913    }
 914
 915    fn handle_agent_servers_updated(
 916        &mut self,
 917        _agent_server_store: &Entity<project::AgentServerStore>,
 918        _event: &project::AgentServersUpdated,
 919        window: &mut Window,
 920        cx: &mut Context<Self>,
 921    ) {
 922        // If we're in a LoadError state OR have a thread_error set (which can happen
 923        // when agent.connect() fails during loading), retry loading the thread.
 924        // This handles the case where a thread is restored before authentication completes.
 925        let should_retry = match &self.server_state {
 926            ServerState::Loading(_) => false,
 927            ServerState::LoadError(_) => true,
 928            ServerState::Connected(connected) => {
 929                connected.auth_state.is_ok() && connected.has_thread_error(cx)
 930            }
 931        };
 932
 933        if should_retry {
 934            if let Some(active) = self.active_thread() {
 935                active.update(cx, |active, cx| {
 936                    active.clear_thread_error(cx);
 937                });
 938            }
 939            self.reset(window, cx);
 940        }
 941    }
 942
 943    pub fn workspace(&self) -> &WeakEntity<Workspace> {
 944        &self.workspace
 945    }
 946
 947    pub fn title(&self, cx: &App) -> SharedString {
 948        match &self.server_state {
 949            ServerState::Connected(_) => "New Thread".into(),
 950            ServerState::Loading(loading_view) => loading_view.read(cx).title.clone(),
 951            ServerState::LoadError(error) => match error {
 952                LoadError::Unsupported { .. } => format!("Upgrade {}", self.agent.name()).into(),
 953                LoadError::FailedToInstall(_) => {
 954                    format!("Failed to Install {}", self.agent.name()).into()
 955                }
 956                LoadError::Exited { .. } => format!("{} Exited", self.agent.name()).into(),
 957                LoadError::Other(_) => format!("Error Loading {}", self.agent.name()).into(),
 958            },
 959        }
 960    }
 961
 962    pub fn cancel_generation(&mut self, cx: &mut Context<Self>) {
 963        if let Some(active) = self.active_thread() {
 964            active.update(cx, |active, cx| {
 965                active.cancel_generation(cx);
 966            });
 967        }
 968    }
 969
 970    pub fn handle_title_editor_event(
 971        &mut self,
 972        title_editor: &Entity<Editor>,
 973        event: &EditorEvent,
 974        window: &mut Window,
 975        cx: &mut Context<Self>,
 976    ) {
 977        if let Some(active) = self.active_thread() {
 978            active.update(cx, |active, cx| {
 979                active.handle_title_editor_event(title_editor, event, window, cx);
 980            });
 981        }
 982    }
 983
 984    pub fn is_loading(&self) -> bool {
 985        matches!(self.server_state, ServerState::Loading { .. })
 986    }
 987
 988    fn update_turn_tokens(&mut self, cx: &mut Context<Self>) {
 989        if let Some(active) = self.active_thread() {
 990            active.update(cx, |active, cx| {
 991                active.update_turn_tokens(cx);
 992            });
 993        }
 994    }
 995
 996    fn send_queued_message_at_index(
 997        &mut self,
 998        index: usize,
 999        is_send_now: bool,
1000        window: &mut Window,
1001        cx: &mut Context<Self>,
1002    ) {
1003        if let Some(active) = self.active_thread() {
1004            active.update(cx, |active, cx| {
1005                active.send_queued_message_at_index(index, is_send_now, window, cx);
1006            });
1007        }
1008    }
1009
1010    fn handle_thread_event(
1011        &mut self,
1012        thread: &Entity<AcpThread>,
1013        event: &AcpThreadEvent,
1014        window: &mut Window,
1015        cx: &mut Context<Self>,
1016    ) {
1017        let thread_id = thread.read(cx).session_id().clone();
1018        let is_subagent = thread.read(cx).parent_session_id().is_some();
1019        match event {
1020            AcpThreadEvent::NewEntry => {
1021                let len = thread.read(cx).entries().len();
1022                let index = len - 1;
1023                if let Some(active) = self.thread_view(&thread_id) {
1024                    let entry_view_state = active.read(cx).entry_view_state.clone();
1025                    let list_state = active.read(cx).list_state.clone();
1026                    entry_view_state.update(cx, |view_state, cx| {
1027                        view_state.sync_entry(index, thread, window, cx);
1028                        list_state.splice_focusable(
1029                            index..index,
1030                            [view_state
1031                                .entry(index)
1032                                .and_then(|entry| entry.focus_handle(cx))],
1033                        );
1034                    });
1035                }
1036            }
1037            AcpThreadEvent::EntryUpdated(index) => {
1038                if let Some(entry_view_state) = self
1039                    .thread_view(&thread_id)
1040                    .map(|active| active.read(cx).entry_view_state.clone())
1041                {
1042                    entry_view_state.update(cx, |view_state, cx| {
1043                        view_state.sync_entry(*index, thread, window, cx)
1044                    });
1045                }
1046            }
1047            AcpThreadEvent::EntriesRemoved(range) => {
1048                if let Some(active) = self.thread_view(&thread_id) {
1049                    let entry_view_state = active.read(cx).entry_view_state.clone();
1050                    let list_state = active.read(cx).list_state.clone();
1051                    entry_view_state.update(cx, |view_state, _cx| view_state.remove(range.clone()));
1052                    list_state.splice(range.clone(), 0);
1053                }
1054            }
1055            AcpThreadEvent::SubagentSpawned(session_id) => self.load_subagent_session(
1056                session_id.clone(),
1057                thread.read(cx).session_id().clone(),
1058                window,
1059                cx,
1060            ),
1061            AcpThreadEvent::ToolAuthorizationRequired => {
1062                self.notify_with_sound("Waiting for tool confirmation", IconName::Info, window, cx);
1063            }
1064            AcpThreadEvent::Retry(retry) => {
1065                if let Some(active) = self.thread_view(&thread_id) {
1066                    active.update(cx, |active, _cx| {
1067                        active.thread_retry_status = Some(retry.clone());
1068                    });
1069                }
1070            }
1071            AcpThreadEvent::Stopped => {
1072                if let Some(active) = self.thread_view(&thread_id) {
1073                    active.update(cx, |active, _cx| {
1074                        active.thread_retry_status.take();
1075                    });
1076                }
1077                if is_subagent {
1078                    return;
1079                }
1080
1081                let used_tools = thread.read(cx).used_tools_since_last_user_message();
1082                self.notify_with_sound(
1083                    if used_tools {
1084                        "Finished running tools"
1085                    } else {
1086                        "New message"
1087                    },
1088                    IconName::ZedAssistant,
1089                    window,
1090                    cx,
1091                );
1092
1093                let should_send_queued = if let Some(active) = self.active_thread() {
1094                    active.update(cx, |active, cx| {
1095                        if active.skip_queue_processing_count > 0 {
1096                            active.skip_queue_processing_count -= 1;
1097                            false
1098                        } else if active.user_interrupted_generation {
1099                            // Manual interruption: don't auto-process queue.
1100                            // Reset the flag so future completions can process normally.
1101                            active.user_interrupted_generation = false;
1102                            false
1103                        } else {
1104                            let has_queued = !active.local_queued_messages.is_empty();
1105                            // Don't auto-send if the first message editor is currently focused
1106                            let is_first_editor_focused = active
1107                                .queued_message_editors
1108                                .first()
1109                                .is_some_and(|editor| editor.focus_handle(cx).is_focused(window));
1110                            has_queued && !is_first_editor_focused
1111                        }
1112                    })
1113                } else {
1114                    false
1115                };
1116                if should_send_queued {
1117                    self.send_queued_message_at_index(0, false, window, cx);
1118                }
1119
1120                self.history.update(cx, |history, cx| history.refresh(cx));
1121            }
1122            AcpThreadEvent::Refusal => {
1123                let error = ThreadError::Refusal;
1124                if let Some(active) = self.thread_view(&thread_id) {
1125                    active.update(cx, |active, cx| {
1126                        active.handle_thread_error(error, cx);
1127                        active.thread_retry_status.take();
1128                    });
1129                }
1130                if !is_subagent {
1131                    let model_or_agent_name = self.current_model_name(cx);
1132                    let notification_message =
1133                        format!("{} refused to respond to this request", model_or_agent_name);
1134                    self.notify_with_sound(&notification_message, IconName::Warning, window, cx);
1135                }
1136            }
1137            AcpThreadEvent::Error => {
1138                if let Some(active) = self.thread_view(&thread_id) {
1139                    active.update(cx, |active, _cx| {
1140                        active.thread_retry_status.take();
1141                    });
1142                }
1143                if !is_subagent {
1144                    self.notify_with_sound(
1145                        "Agent stopped due to an error",
1146                        IconName::Warning,
1147                        window,
1148                        cx,
1149                    );
1150                }
1151            }
1152            AcpThreadEvent::LoadError(error) => {
1153                match &self.server_state {
1154                    ServerState::Connected(connected) => {
1155                        if connected
1156                            .current
1157                            .read(cx)
1158                            .message_editor
1159                            .focus_handle(cx)
1160                            .is_focused(window)
1161                        {
1162                            self.focus_handle.focus(window, cx)
1163                        }
1164                    }
1165                    _ => {}
1166                }
1167                self.set_server_state(ServerState::LoadError(error.clone()), cx);
1168            }
1169            AcpThreadEvent::TitleUpdated => {
1170                let title = thread.read(cx).title();
1171                if let Some(title_editor) = self
1172                    .thread_view(&thread_id)
1173                    .and_then(|active| active.read(cx).title_editor.clone())
1174                {
1175                    title_editor.update(cx, |editor, cx| {
1176                        if editor.text(cx) != title {
1177                            editor.set_text(title, window, cx);
1178                        }
1179                    });
1180                }
1181                self.history.update(cx, |history, cx| history.refresh(cx));
1182            }
1183            AcpThreadEvent::PromptCapabilitiesUpdated => {
1184                if let Some(active) = self.thread_view(&thread_id) {
1185                    active.update(cx, |active, _cx| {
1186                        active
1187                            .prompt_capabilities
1188                            .replace(thread.read(_cx).prompt_capabilities());
1189                    });
1190                }
1191            }
1192            AcpThreadEvent::TokenUsageUpdated => {
1193                self.update_turn_tokens(cx);
1194                self.emit_token_limit_telemetry_if_needed(thread, cx);
1195            }
1196            AcpThreadEvent::AvailableCommandsUpdated(available_commands) => {
1197                let mut available_commands = available_commands.clone();
1198
1199                if thread
1200                    .read(cx)
1201                    .connection()
1202                    .auth_methods()
1203                    .iter()
1204                    .any(|method| method.id.0.as_ref() == "claude-login")
1205                {
1206                    available_commands.push(acp::AvailableCommand::new("login", "Authenticate"));
1207                    available_commands.push(acp::AvailableCommand::new("logout", "Authenticate"));
1208                }
1209
1210                let has_commands = !available_commands.is_empty();
1211                if let Some(active) = self.active_thread() {
1212                    active.update(cx, |active, _cx| {
1213                        active.available_commands.replace(available_commands);
1214                    });
1215                }
1216
1217                let agent_display_name = self
1218                    .agent_server_store
1219                    .read(cx)
1220                    .agent_display_name(&ExternalAgentServerName(self.agent.name()))
1221                    .unwrap_or_else(|| self.agent.name());
1222
1223                if let Some(active) = self.active_thread() {
1224                    let new_placeholder =
1225                        placeholder_text(agent_display_name.as_ref(), has_commands);
1226                    active.update(cx, |active, cx| {
1227                        active.message_editor.update(cx, |editor, cx| {
1228                            editor.set_placeholder_text(&new_placeholder, window, cx);
1229                        });
1230                    });
1231                }
1232            }
1233            AcpThreadEvent::ModeUpdated(_mode) => {
1234                // The connection keeps track of the mode
1235                cx.notify();
1236            }
1237            AcpThreadEvent::ConfigOptionsUpdated(_) => {
1238                // The watch task in ConfigOptionsView handles rebuilding selectors
1239                cx.notify();
1240            }
1241        }
1242        cx.notify();
1243    }
1244
1245    fn authenticate(
1246        &mut self,
1247        method: acp::AuthMethodId,
1248        window: &mut Window,
1249        cx: &mut Context<Self>,
1250    ) {
1251        let Some(connected) = self.as_connected_mut() else {
1252            return;
1253        };
1254        let connection = connected.connection.clone();
1255
1256        let AuthState::Unauthenticated {
1257            configuration_view,
1258            pending_auth_method,
1259            ..
1260        } = &mut connected.auth_state
1261        else {
1262            return;
1263        };
1264
1265        let agent_telemetry_id = connection.telemetry_id();
1266
1267        // Check for the experimental "terminal-auth" _meta field
1268        let auth_method = connection.auth_methods().iter().find(|m| m.id == method);
1269
1270        if let Some(terminal_auth) = auth_method
1271            .and_then(|a| a.meta.as_ref())
1272            .and_then(|m| m.get("terminal-auth"))
1273        {
1274            // Extract terminal auth details from meta
1275            if let (Some(command), Some(label)) = (
1276                terminal_auth.get("command").and_then(|v| v.as_str()),
1277                terminal_auth.get("label").and_then(|v| v.as_str()),
1278            ) {
1279                let args = terminal_auth
1280                    .get("args")
1281                    .and_then(|v| v.as_array())
1282                    .map(|arr| {
1283                        arr.iter()
1284                            .filter_map(|v| v.as_str().map(String::from))
1285                            .collect()
1286                    })
1287                    .unwrap_or_default();
1288
1289                let env = terminal_auth
1290                    .get("env")
1291                    .and_then(|v| v.as_object())
1292                    .map(|obj| {
1293                        obj.iter()
1294                            .filter_map(|(k, v)| v.as_str().map(|val| (k.clone(), val.to_string())))
1295                            .collect::<HashMap<String, String>>()
1296                    })
1297                    .unwrap_or_default();
1298
1299                // Run SpawnInTerminal in the same dir as the ACP server
1300                let cwd = connected
1301                    .connection
1302                    .clone()
1303                    .downcast::<agent_servers::AcpConnection>()
1304                    .map(|acp_conn| acp_conn.root_dir().to_path_buf());
1305
1306                // Build SpawnInTerminal from _meta
1307                let login = task::SpawnInTerminal {
1308                    id: task::TaskId(format!("external-agent-{}-login", label)),
1309                    full_label: label.to_string(),
1310                    label: label.to_string(),
1311                    command: Some(command.to_string()),
1312                    args,
1313                    command_label: label.to_string(),
1314                    cwd,
1315                    env,
1316                    use_new_terminal: true,
1317                    allow_concurrent_runs: true,
1318                    hide: task::HideStrategy::Always,
1319                    ..Default::default()
1320                };
1321
1322                configuration_view.take();
1323                pending_auth_method.replace(method.clone());
1324
1325                if let Some(workspace) = self.workspace.upgrade() {
1326                    let project = self.project.clone();
1327                    let authenticate = Self::spawn_external_agent_login(
1328                        login,
1329                        workspace,
1330                        project,
1331                        method.clone(),
1332                        false,
1333                        window,
1334                        cx,
1335                    );
1336                    cx.notify();
1337                    self.auth_task = Some(cx.spawn_in(window, {
1338                        async move |this, cx| {
1339                            let result = authenticate.await;
1340
1341                            match &result {
1342                                Ok(_) => telemetry::event!(
1343                                    "Authenticate Agent Succeeded",
1344                                    agent = agent_telemetry_id
1345                                ),
1346                                Err(_) => {
1347                                    telemetry::event!(
1348                                        "Authenticate Agent Failed",
1349                                        agent = agent_telemetry_id,
1350                                    )
1351                                }
1352                            }
1353
1354                            this.update_in(cx, |this, window, cx| {
1355                                if let Err(err) = result {
1356                                    if let Some(ConnectedServerState {
1357                                        auth_state:
1358                                            AuthState::Unauthenticated {
1359                                                pending_auth_method,
1360                                                ..
1361                                            },
1362                                        ..
1363                                    }) = this.as_connected_mut()
1364                                    {
1365                                        pending_auth_method.take();
1366                                    }
1367                                    if let Some(active) = this.active_thread() {
1368                                        active.update(cx, |active, cx| {
1369                                            active.handle_any_thread_error(err, cx);
1370                                        })
1371                                    }
1372                                } else {
1373                                    this.reset(window, cx);
1374                                }
1375                                this.auth_task.take()
1376                            })
1377                            .ok();
1378                        }
1379                    }));
1380                }
1381                return;
1382            }
1383        }
1384
1385        if method.0.as_ref() == "gemini-api-key" {
1386            let registry = LanguageModelRegistry::global(cx);
1387            let provider = registry
1388                .read(cx)
1389                .provider(&language_model::GOOGLE_PROVIDER_ID)
1390                .unwrap();
1391            if !provider.is_authenticated(cx) {
1392                let this = cx.weak_entity();
1393                let agent_name = self.agent.name();
1394                window.defer(cx, |window, cx| {
1395                    Self::handle_auth_required(
1396                        this,
1397                        AuthRequired {
1398                            description: Some("GEMINI_API_KEY must be set".to_owned()),
1399                            provider_id: Some(language_model::GOOGLE_PROVIDER_ID),
1400                        },
1401                        agent_name,
1402                        window,
1403                        cx,
1404                    );
1405                });
1406                return;
1407            }
1408        } else if method.0.as_ref() == "vertex-ai"
1409            && std::env::var("GOOGLE_API_KEY").is_err()
1410            && (std::env::var("GOOGLE_CLOUD_PROJECT").is_err()
1411                || (std::env::var("GOOGLE_CLOUD_PROJECT").is_err()))
1412        {
1413            let this = cx.weak_entity();
1414            let agent_name = self.agent.name();
1415
1416            window.defer(cx, |window, cx| {
1417                    Self::handle_auth_required(
1418                        this,
1419                        AuthRequired {
1420                            description: Some(
1421                                "GOOGLE_API_KEY must be set in the environment to use Vertex AI authentication for Gemini CLI. Please export it and restart Zed."
1422                                    .to_owned(),
1423                            ),
1424                            provider_id: None,
1425                        },
1426                        agent_name,
1427                        window,
1428                        cx,
1429                    )
1430                });
1431            return;
1432        }
1433
1434        configuration_view.take();
1435        pending_auth_method.replace(method.clone());
1436        let authenticate = if let Some(login) = self.login.clone() {
1437            if let Some(workspace) = self.workspace.upgrade() {
1438                let project = self.project.clone();
1439                Self::spawn_external_agent_login(
1440                    login,
1441                    workspace,
1442                    project,
1443                    method.clone(),
1444                    false,
1445                    window,
1446                    cx,
1447                )
1448            } else {
1449                Task::ready(Ok(()))
1450            }
1451        } else {
1452            connection.authenticate(method, cx)
1453        };
1454        cx.notify();
1455        self.auth_task = Some(cx.spawn_in(window, {
1456            async move |this, cx| {
1457                let result = authenticate.await;
1458
1459                match &result {
1460                    Ok(_) => telemetry::event!(
1461                        "Authenticate Agent Succeeded",
1462                        agent = agent_telemetry_id
1463                    ),
1464                    Err(_) => {
1465                        telemetry::event!("Authenticate Agent Failed", agent = agent_telemetry_id,)
1466                    }
1467                }
1468
1469                this.update_in(cx, |this, window, cx| {
1470                    if let Err(err) = result {
1471                        if let Some(ConnectedServerState {
1472                            auth_state:
1473                                AuthState::Unauthenticated {
1474                                    pending_auth_method,
1475                                    ..
1476                                },
1477                            ..
1478                        }) = this.as_connected_mut()
1479                        {
1480                            pending_auth_method.take();
1481                        }
1482                        if let Some(active) = this.active_thread() {
1483                            active.update(cx, |active, cx| active.handle_any_thread_error(err, cx));
1484                        }
1485                    } else {
1486                        this.reset(window, cx);
1487                    }
1488                    this.auth_task.take()
1489                })
1490                .ok();
1491            }
1492        }));
1493    }
1494
1495    fn load_subagent_session(
1496        &mut self,
1497        subagent_id: acp::SessionId,
1498        parent_id: acp::SessionId,
1499        window: &mut Window,
1500        cx: &mut Context<Self>,
1501    ) {
1502        let Some(connected) = self.as_connected() else {
1503            return;
1504        };
1505        if connected.threads.contains_key(&subagent_id)
1506            || !connected.connection.supports_load_session(cx)
1507        {
1508            return;
1509        }
1510        let root_dir = self
1511            .project
1512            .read(cx)
1513            .worktrees(cx)
1514            .filter_map(|worktree| {
1515                if worktree.read(cx).is_single_file() {
1516                    Some(worktree.read(cx).abs_path().parent()?.into())
1517                } else {
1518                    Some(worktree.read(cx).abs_path())
1519                }
1520            })
1521            .next();
1522        let cwd = root_dir.unwrap_or_else(|| paths::home_dir().as_path().into());
1523
1524        let subagent_thread_task = connected.connection.clone().load_session(
1525            AgentSessionInfo::new(subagent_id.clone()),
1526            self.project.clone(),
1527            &cwd,
1528            cx,
1529        );
1530
1531        cx.spawn_in(window, async move |this, cx| {
1532            let subagent_thread = subagent_thread_task.await?;
1533            this.update_in(cx, |this, window, cx| {
1534                let view = this.new_thread_view(
1535                    Some(parent_id),
1536                    subagent_thread,
1537                    false,
1538                    None,
1539                    None,
1540                    window,
1541                    cx,
1542                );
1543                let Some(connected) = this.as_connected_mut() else {
1544                    return;
1545                };
1546                connected.threads.insert(subagent_id, view);
1547            })
1548        })
1549        .detach();
1550    }
1551
1552    fn spawn_external_agent_login(
1553        login: task::SpawnInTerminal,
1554        workspace: Entity<Workspace>,
1555        project: Entity<Project>,
1556        method: acp::AuthMethodId,
1557        previous_attempt: bool,
1558        window: &mut Window,
1559        cx: &mut App,
1560    ) -> Task<Result<()>> {
1561        let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
1562            return Task::ready(Ok(()));
1563        };
1564
1565        window.spawn(cx, async move |cx| {
1566            let mut task = login.clone();
1567            if let Some(cmd) = &task.command {
1568                // Have "node" command use Zed's managed Node runtime by default
1569                if cmd == "node" {
1570                    let resolved_node_runtime = project
1571                        .update(cx, |project, cx| {
1572                            let agent_server_store = project.agent_server_store().clone();
1573                            agent_server_store.update(cx, |store, cx| {
1574                                store.node_runtime().map(|node_runtime| {
1575                                    cx.background_spawn(async move {
1576                                        node_runtime.binary_path().await
1577                                    })
1578                                })
1579                            })
1580                        });
1581
1582                    if let Some(resolve_task) = resolved_node_runtime {
1583                        if let Ok(node_path) = resolve_task.await {
1584                            task.command = Some(node_path.to_string_lossy().to_string());
1585                        }
1586                    }
1587                }
1588            }
1589            task.shell = task::Shell::WithArguments {
1590                program: task.command.take().expect("login command should be set"),
1591                args: std::mem::take(&mut task.args),
1592                title_override: None
1593            };
1594            task.full_label = task.label.clone();
1595            task.id = task::TaskId(format!("external-agent-{}-login", task.label));
1596            task.command_label = task.label.clone();
1597            task.use_new_terminal = true;
1598            task.allow_concurrent_runs = true;
1599            task.hide = task::HideStrategy::Always;
1600
1601            let terminal = terminal_panel
1602                .update_in(cx, |terminal_panel, window, cx| {
1603                    terminal_panel.spawn_task(&task, window, cx)
1604                })?
1605                .await?;
1606
1607            let success_patterns = match method.0.as_ref() {
1608                "claude-login" | "spawn-gemini-cli" => vec![
1609                    "Login successful".to_string(),
1610                    "Type your message".to_string(),
1611                ],
1612                _ => Vec::new(),
1613            };
1614            if success_patterns.is_empty() {
1615                // No success patterns specified: wait for the process to exit and check exit code
1616                let exit_status = terminal
1617                    .read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
1618                    .await;
1619
1620                match exit_status {
1621                    Some(status) if status.success() => Ok(()),
1622                    Some(status) => Err(anyhow!(
1623                        "Login command failed with exit code: {:?}",
1624                        status.code()
1625                    )),
1626                    None => Err(anyhow!("Login command terminated without exit status")),
1627                }
1628            } else {
1629                // Look for specific output patterns to detect successful login
1630                let mut exit_status = terminal
1631                    .read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
1632                    .fuse();
1633
1634                let logged_in = cx
1635                    .spawn({
1636                        let terminal = terminal.clone();
1637                        async move |cx| {
1638                            loop {
1639                                cx.background_executor().timer(Duration::from_secs(1)).await;
1640                                let content =
1641                                    terminal.update(cx, |terminal, _cx| terminal.get_content())?;
1642                                if success_patterns.iter().any(|pattern| content.contains(pattern))
1643                                {
1644                                    return anyhow::Ok(());
1645                                }
1646                            }
1647                        }
1648                    })
1649                    .fuse();
1650                futures::pin_mut!(logged_in);
1651                futures::select_biased! {
1652                    result = logged_in => {
1653                        if let Err(e) = result {
1654                            log::error!("{e}");
1655                            return Err(anyhow!("exited before logging in"));
1656                        }
1657                    }
1658                    _ = exit_status => {
1659                        if !previous_attempt && project.read_with(cx, |project, _| project.is_via_remote_server()) && login.label.contains("gemini") {
1660                            return cx.update(|window, cx| Self::spawn_external_agent_login(login, workspace, project.clone(), method, true, window, cx))?.await
1661                        }
1662                        return Err(anyhow!("exited before logging in"));
1663                    }
1664                }
1665                terminal.update(cx, |terminal, _| terminal.kill_active_task())?;
1666                Ok(())
1667            }
1668        })
1669    }
1670
1671    pub fn has_user_submitted_prompt(&self, cx: &App) -> bool {
1672        self.active_thread().is_some_and(|active| {
1673            active
1674                .read(cx)
1675                .thread
1676                .read(cx)
1677                .entries()
1678                .iter()
1679                .any(|entry| {
1680                    matches!(
1681                        entry,
1682                        AgentThreadEntry::UserMessage(user_message) if user_message.id.is_some()
1683                    )
1684                })
1685        })
1686    }
1687
1688    fn render_auth_required_state(
1689        &self,
1690        connection: &Rc<dyn AgentConnection>,
1691        description: Option<&Entity<Markdown>>,
1692        configuration_view: Option<&AnyView>,
1693        pending_auth_method: Option<&acp::AuthMethodId>,
1694        window: &mut Window,
1695        cx: &Context<Self>,
1696    ) -> impl IntoElement {
1697        let auth_methods = connection.auth_methods();
1698
1699        let agent_display_name = self
1700            .agent_server_store
1701            .read(cx)
1702            .agent_display_name(&ExternalAgentServerName(self.agent.name()))
1703            .unwrap_or_else(|| self.agent.name());
1704
1705        let show_fallback_description = auth_methods.len() > 1
1706            && configuration_view.is_none()
1707            && description.is_none()
1708            && pending_auth_method.is_none();
1709
1710        let auth_buttons = || {
1711            h_flex().justify_end().flex_wrap().gap_1().children(
1712                connection
1713                    .auth_methods()
1714                    .iter()
1715                    .enumerate()
1716                    .rev()
1717                    .map(|(ix, method)| {
1718                        let (method_id, name) = if self.project.read(cx).is_via_remote_server()
1719                            && method.id.0.as_ref() == "oauth-personal"
1720                            && method.name == "Log in with Google"
1721                        {
1722                            ("spawn-gemini-cli".into(), "Log in with Gemini CLI".into())
1723                        } else {
1724                            (method.id.0.clone(), method.name.clone())
1725                        };
1726
1727                        let agent_telemetry_id = connection.telemetry_id();
1728
1729                        Button::new(method_id.clone(), name)
1730                            .label_size(LabelSize::Small)
1731                            .map(|this| {
1732                                if ix == 0 {
1733                                    this.style(ButtonStyle::Tinted(TintColor::Accent))
1734                                } else {
1735                                    this.style(ButtonStyle::Outlined)
1736                                }
1737                            })
1738                            .when_some(method.description.clone(), |this, description| {
1739                                this.tooltip(Tooltip::text(description))
1740                            })
1741                            .on_click({
1742                                cx.listener(move |this, _, window, cx| {
1743                                    telemetry::event!(
1744                                        "Authenticate Agent Started",
1745                                        agent = agent_telemetry_id,
1746                                        method = method_id
1747                                    );
1748
1749                                    this.authenticate(
1750                                        acp::AuthMethodId::new(method_id.clone()),
1751                                        window,
1752                                        cx,
1753                                    )
1754                                })
1755                            })
1756                    }),
1757            )
1758        };
1759
1760        if pending_auth_method.is_some() {
1761            return Callout::new()
1762                .icon(IconName::Info)
1763                .title(format!("Authenticating to {}", agent_display_name))
1764                .actions_slot(
1765                    Icon::new(IconName::ArrowCircle)
1766                        .size(IconSize::Small)
1767                        .color(Color::Muted)
1768                        .with_rotate_animation(2)
1769                        .into_any_element(),
1770                )
1771                .into_any_element();
1772        }
1773
1774        Callout::new()
1775            .icon(IconName::Info)
1776            .title(format!("Authenticate to {}", agent_display_name))
1777            .when(auth_methods.len() == 1, |this| {
1778                this.actions_slot(auth_buttons())
1779            })
1780            .description_slot(
1781                v_flex()
1782                    .text_ui(cx)
1783                    .map(|this| {
1784                        if show_fallback_description {
1785                            this.child(
1786                                Label::new("Choose one of the following authentication options:")
1787                                    .size(LabelSize::Small)
1788                                    .color(Color::Muted),
1789                            )
1790                        } else {
1791                            this.children(
1792                                configuration_view
1793                                    .cloned()
1794                                    .map(|view| div().w_full().child(view)),
1795                            )
1796                            .children(description.map(|desc| {
1797                                self.render_markdown(
1798                                    desc.clone(),
1799                                    MarkdownStyle::themed(MarkdownFont::Agent, window, cx),
1800                                )
1801                            }))
1802                        }
1803                    })
1804                    .when(auth_methods.len() > 1, |this| {
1805                        this.gap_1().child(auth_buttons())
1806                    }),
1807            )
1808            .into_any_element()
1809    }
1810
1811    fn emit_token_limit_telemetry_if_needed(
1812        &mut self,
1813        thread: &Entity<AcpThread>,
1814        cx: &mut Context<Self>,
1815    ) {
1816        let Some(active_thread) = self.active_thread() else {
1817            return;
1818        };
1819
1820        let (ratio, agent_telemetry_id, session_id) = {
1821            let thread_data = thread.read(cx);
1822            let Some(token_usage) = thread_data.token_usage() else {
1823                return;
1824            };
1825            (
1826                token_usage.ratio(),
1827                thread_data.connection().telemetry_id(),
1828                thread_data.session_id().clone(),
1829            )
1830        };
1831
1832        let kind = match ratio {
1833            acp_thread::TokenUsageRatio::Normal => {
1834                active_thread.update(cx, |active, _cx| {
1835                    active.last_token_limit_telemetry = None;
1836                });
1837                return;
1838            }
1839            acp_thread::TokenUsageRatio::Warning => "warning",
1840            acp_thread::TokenUsageRatio::Exceeded => "exceeded",
1841        };
1842
1843        let should_skip = active_thread
1844            .read(cx)
1845            .last_token_limit_telemetry
1846            .as_ref()
1847            .is_some_and(|last| *last >= ratio);
1848        if should_skip {
1849            return;
1850        }
1851
1852        active_thread.update(cx, |active, _cx| {
1853            active.last_token_limit_telemetry = Some(ratio);
1854        });
1855
1856        telemetry::event!(
1857            "Agent Token Limit Warning",
1858            agent = agent_telemetry_id,
1859            session_id = session_id,
1860            kind = kind,
1861        );
1862    }
1863
1864    fn emit_load_error_telemetry(&self, error: &LoadError) {
1865        let error_kind = match error {
1866            LoadError::Unsupported { .. } => "unsupported",
1867            LoadError::FailedToInstall(_) => "failed_to_install",
1868            LoadError::Exited { .. } => "exited",
1869            LoadError::Other(_) => "other",
1870        };
1871
1872        let agent_name = self.agent.name();
1873
1874        telemetry::event!(
1875            "Agent Panel Error Shown",
1876            agent = agent_name,
1877            kind = error_kind,
1878            message = error.to_string(),
1879        );
1880    }
1881
1882    fn render_load_error(
1883        &self,
1884        e: &LoadError,
1885        window: &mut Window,
1886        cx: &mut Context<Self>,
1887    ) -> AnyElement {
1888        let (title, message, action_slot): (_, SharedString, _) = match e {
1889            LoadError::Unsupported {
1890                command: path,
1891                current_version,
1892                minimum_version,
1893            } => {
1894                return self.render_unsupported(path, current_version, minimum_version, window, cx);
1895            }
1896            LoadError::FailedToInstall(msg) => (
1897                "Failed to Install",
1898                msg.into(),
1899                Some(self.create_copy_button(msg.to_string()).into_any_element()),
1900            ),
1901            LoadError::Exited { status } => (
1902                "Failed to Launch",
1903                format!("Server exited with status {status}").into(),
1904                None,
1905            ),
1906            LoadError::Other(msg) => (
1907                "Failed to Launch",
1908                msg.into(),
1909                Some(self.create_copy_button(msg.to_string()).into_any_element()),
1910            ),
1911        };
1912
1913        Callout::new()
1914            .severity(Severity::Error)
1915            .icon(IconName::XCircleFilled)
1916            .title(title)
1917            .description(message)
1918            .actions_slot(div().children(action_slot))
1919            .into_any_element()
1920    }
1921
1922    fn render_unsupported(
1923        &self,
1924        path: &SharedString,
1925        version: &SharedString,
1926        minimum_version: &SharedString,
1927        _window: &mut Window,
1928        cx: &mut Context<Self>,
1929    ) -> AnyElement {
1930        let (heading_label, description_label) = (
1931            format!("Upgrade {} to work with Zed", self.agent.name()),
1932            if version.is_empty() {
1933                format!(
1934                    "Currently using {}, which does not report a valid --version",
1935                    path,
1936                )
1937            } else {
1938                format!(
1939                    "Currently using {}, which is only version {} (need at least {minimum_version})",
1940                    path, version
1941                )
1942            },
1943        );
1944
1945        v_flex()
1946            .w_full()
1947            .p_3p5()
1948            .gap_2p5()
1949            .border_t_1()
1950            .border_color(cx.theme().colors().border)
1951            .bg(linear_gradient(
1952                180.,
1953                linear_color_stop(cx.theme().colors().editor_background.opacity(0.4), 4.),
1954                linear_color_stop(cx.theme().status().info_background.opacity(0.), 0.),
1955            ))
1956            .child(
1957                v_flex().gap_0p5().child(Label::new(heading_label)).child(
1958                    Label::new(description_label)
1959                        .size(LabelSize::Small)
1960                        .color(Color::Muted),
1961                ),
1962            )
1963            .into_any_element()
1964    }
1965
1966    pub(crate) fn as_native_connection(
1967        &self,
1968        cx: &App,
1969    ) -> Option<Rc<agent::NativeAgentConnection>> {
1970        let acp_thread = self.active_thread()?.read(cx).thread.read(cx);
1971        acp_thread.connection().clone().downcast()
1972    }
1973
1974    pub(crate) fn as_native_thread(&self, cx: &App) -> Option<Entity<agent::Thread>> {
1975        let acp_thread = self.active_thread()?.read(cx).thread.read(cx);
1976        self.as_native_connection(cx)?
1977            .thread(acp_thread.session_id(), cx)
1978    }
1979
1980    fn queued_messages_len(&self, cx: &App) -> usize {
1981        self.active_thread()
1982            .map(|thread| thread.read(cx).local_queued_messages.len())
1983            .unwrap_or_default()
1984    }
1985
1986    fn update_queued_message(
1987        &mut self,
1988        index: usize,
1989        content: Vec<acp::ContentBlock>,
1990        tracked_buffers: Vec<Entity<Buffer>>,
1991        cx: &mut Context<Self>,
1992    ) -> bool {
1993        match self.active_thread() {
1994            Some(thread) => thread.update(cx, |thread, _cx| {
1995                if index < thread.local_queued_messages.len() {
1996                    thread.local_queued_messages[index] = QueuedMessage {
1997                        content,
1998                        tracked_buffers,
1999                    };
2000                    true
2001                } else {
2002                    false
2003                }
2004            }),
2005            None => false,
2006        }
2007    }
2008
2009    fn queued_message_contents(&self, cx: &App) -> Vec<Vec<acp::ContentBlock>> {
2010        match self.active_thread() {
2011            None => Vec::new(),
2012            Some(thread) => thread
2013                .read(cx)
2014                .local_queued_messages
2015                .iter()
2016                .map(|q| q.content.clone())
2017                .collect(),
2018        }
2019    }
2020
2021    fn save_queued_message_at_index(&mut self, index: usize, cx: &mut Context<Self>) {
2022        let editor = match self.active_thread() {
2023            Some(thread) => thread.read(cx).queued_message_editors.get(index).cloned(),
2024            None => None,
2025        };
2026        let Some(editor) = editor else {
2027            return;
2028        };
2029
2030        let contents_task = editor.update(cx, |editor, cx| editor.contents(false, cx));
2031
2032        cx.spawn(async move |this, cx| {
2033            let Ok((content, tracked_buffers)) = contents_task.await else {
2034                return Ok::<(), anyhow::Error>(());
2035            };
2036
2037            this.update(cx, |this, cx| {
2038                this.update_queued_message(index, content, tracked_buffers, cx);
2039                cx.notify();
2040            })?;
2041
2042            Ok(())
2043        })
2044        .detach_and_log_err(cx);
2045    }
2046
2047    fn sync_queued_message_editors(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2048        let needed_count = self.queued_messages_len(cx);
2049        let queued_messages = self.queued_message_contents(cx);
2050
2051        let agent_name = self.agent.name();
2052        let workspace = self.workspace.clone();
2053        let project = self.project.downgrade();
2054        let history = self.history.downgrade();
2055
2056        let Some(thread) = self.active_thread() else {
2057            return;
2058        };
2059        let prompt_capabilities = thread.read(cx).prompt_capabilities.clone();
2060        let available_commands = thread.read(cx).available_commands.clone();
2061
2062        let current_count = thread.read(cx).queued_message_editors.len();
2063        let last_synced = thread.read(cx).last_synced_queue_length;
2064
2065        if current_count == needed_count && needed_count == last_synced {
2066            return;
2067        }
2068
2069        if current_count > needed_count {
2070            thread.update(cx, |thread, _cx| {
2071                thread.queued_message_editors.truncate(needed_count);
2072                thread
2073                    .queued_message_editor_subscriptions
2074                    .truncate(needed_count);
2075            });
2076
2077            let editors = thread.read(cx).queued_message_editors.clone();
2078            for (index, editor) in editors.into_iter().enumerate() {
2079                if let Some(content) = queued_messages.get(index) {
2080                    editor.update(cx, |editor, cx| {
2081                        editor.set_message(content.clone(), window, cx);
2082                    });
2083                }
2084            }
2085        }
2086
2087        while thread.read(cx).queued_message_editors.len() < needed_count {
2088            let index = thread.read(cx).queued_message_editors.len();
2089            let content = queued_messages.get(index).cloned().unwrap_or_default();
2090
2091            let editor = cx.new(|cx| {
2092                let mut editor = MessageEditor::new(
2093                    workspace.clone(),
2094                    project.clone(),
2095                    None,
2096                    history.clone(),
2097                    None,
2098                    prompt_capabilities.clone(),
2099                    available_commands.clone(),
2100                    agent_name.clone(),
2101                    "",
2102                    EditorMode::AutoHeight {
2103                        min_lines: 1,
2104                        max_lines: Some(10),
2105                    },
2106                    window,
2107                    cx,
2108                );
2109                editor.set_message(content, window, cx);
2110                editor
2111            });
2112
2113            let subscription = cx.subscribe_in(
2114                &editor,
2115                window,
2116                move |this, _editor, event, window, cx| match event {
2117                    MessageEditorEvent::LostFocus => {
2118                        this.save_queued_message_at_index(index, cx);
2119                    }
2120                    MessageEditorEvent::Cancel => {
2121                        window.focus(&this.focus_handle(cx), cx);
2122                    }
2123                    MessageEditorEvent::Send => {
2124                        window.focus(&this.focus_handle(cx), cx);
2125                    }
2126                    MessageEditorEvent::SendImmediately => {
2127                        this.send_queued_message_at_index(index, true, window, cx);
2128                    }
2129                    _ => {}
2130                },
2131            );
2132
2133            thread.update(cx, |thread, _cx| {
2134                thread.queued_message_editors.push(editor);
2135                thread
2136                    .queued_message_editor_subscriptions
2137                    .push(subscription);
2138            });
2139        }
2140
2141        if let Some(active) = self.active_thread() {
2142            active.update(cx, |active, _cx| {
2143                active.last_synced_queue_length = needed_count;
2144            });
2145        }
2146    }
2147
2148    fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
2149        let workspace = self.workspace.clone();
2150        MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| {
2151            crate::acp::thread_view::active_thread::open_link(text, &workspace, window, cx);
2152        })
2153    }
2154
2155    fn notify_with_sound(
2156        &mut self,
2157        caption: impl Into<SharedString>,
2158        icon: IconName,
2159        window: &mut Window,
2160        cx: &mut Context<Self>,
2161    ) {
2162        self.play_notification_sound(window, cx);
2163        self.show_notification(caption, icon, window, cx);
2164    }
2165
2166    fn agent_is_visible(&self, window: &Window, cx: &App) -> bool {
2167        if window.is_window_active() {
2168            let workspace_is_foreground = window
2169                .root::<MultiWorkspace>()
2170                .flatten()
2171                .and_then(|mw| {
2172                    let mw = mw.read(cx);
2173                    self.workspace.upgrade().map(|ws| mw.workspace() == &ws)
2174                })
2175                .unwrap_or(true);
2176
2177            if workspace_is_foreground {
2178                if let Some(workspace) = self.workspace.upgrade() {
2179                    return AgentPanel::is_visible(&workspace, cx);
2180                }
2181            }
2182        }
2183
2184        false
2185    }
2186
2187    fn play_notification_sound(&self, window: &Window, cx: &mut App) {
2188        let settings = AgentSettings::get_global(cx);
2189        if settings.play_sound_when_agent_done && !self.agent_is_visible(window, cx) {
2190            Audio::play_sound(Sound::AgentDone, cx);
2191        }
2192    }
2193
2194    fn show_notification(
2195        &mut self,
2196        caption: impl Into<SharedString>,
2197        icon: IconName,
2198        window: &mut Window,
2199        cx: &mut Context<Self>,
2200    ) {
2201        if !self.notifications.is_empty() {
2202            return;
2203        }
2204
2205        let settings = AgentSettings::get_global(cx);
2206
2207        let should_notify = !self.agent_is_visible(window, cx);
2208
2209        if !should_notify {
2210            return;
2211        }
2212
2213        // TODO: Change this once we have title summarization for external agents.
2214        let title = self.agent.name();
2215
2216        match settings.notify_when_agent_waiting {
2217            NotifyWhenAgentWaiting::PrimaryScreen => {
2218                if let Some(primary) = cx.primary_display() {
2219                    self.pop_up(icon, caption.into(), title, window, primary, cx);
2220                }
2221            }
2222            NotifyWhenAgentWaiting::AllScreens => {
2223                let caption = caption.into();
2224                for screen in cx.displays() {
2225                    self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx);
2226                }
2227            }
2228            NotifyWhenAgentWaiting::Never => {
2229                // Don't show anything
2230            }
2231        }
2232    }
2233
2234    fn pop_up(
2235        &mut self,
2236        icon: IconName,
2237        caption: SharedString,
2238        title: SharedString,
2239        window: &mut Window,
2240        screen: Rc<dyn PlatformDisplay>,
2241        cx: &mut Context<Self>,
2242    ) {
2243        let options = AgentNotification::window_options(screen, cx);
2244
2245        let project_name = self.workspace.upgrade().and_then(|workspace| {
2246            workspace
2247                .read(cx)
2248                .project()
2249                .read(cx)
2250                .visible_worktrees(cx)
2251                .next()
2252                .map(|worktree| worktree.read(cx).root_name_str().to_string())
2253        });
2254
2255        if let Some(screen_window) = cx
2256            .open_window(options, |_window, cx| {
2257                cx.new(|_cx| {
2258                    AgentNotification::new(title.clone(), caption.clone(), icon, project_name)
2259                })
2260            })
2261            .log_err()
2262            && let Some(pop_up) = screen_window.entity(cx).log_err()
2263        {
2264            self.notification_subscriptions
2265                .entry(screen_window)
2266                .or_insert_with(Vec::new)
2267                .push(cx.subscribe_in(&pop_up, window, {
2268                    |this, _, event, window, cx| match event {
2269                        AgentNotificationEvent::Accepted => {
2270                            let Some(handle) = window.window_handle().downcast::<MultiWorkspace>()
2271                            else {
2272                                log::error!("root view should be a MultiWorkspace");
2273                                return;
2274                            };
2275                            cx.activate(true);
2276
2277                            let workspace_handle = this.workspace.clone();
2278
2279                            cx.defer(move |cx| {
2280                                handle
2281                                    .update(cx, |multi_workspace, window, cx| {
2282                                        window.activate_window();
2283                                        if let Some(workspace) = workspace_handle.upgrade() {
2284                                            multi_workspace.activate(workspace.clone(), cx);
2285                                            workspace.update(cx, |workspace, cx| {
2286                                                workspace.focus_panel::<AgentPanel>(window, cx);
2287                                            });
2288                                        }
2289                                    })
2290                                    .log_err();
2291                            });
2292
2293                            this.dismiss_notifications(cx);
2294                        }
2295                        AgentNotificationEvent::Dismissed => {
2296                            this.dismiss_notifications(cx);
2297                        }
2298                    }
2299                }));
2300
2301            self.notifications.push(screen_window);
2302
2303            // If the user manually refocuses the original window, dismiss the popup.
2304            self.notification_subscriptions
2305                .entry(screen_window)
2306                .or_insert_with(Vec::new)
2307                .push({
2308                    let pop_up_weak = pop_up.downgrade();
2309
2310                    cx.observe_window_activation(window, move |this, window, cx| {
2311                        if this.agent_is_visible(window, cx)
2312                            && let Some(pop_up) = pop_up_weak.upgrade()
2313                        {
2314                            pop_up.update(cx, |notification, cx| {
2315                                notification.dismiss(cx);
2316                            });
2317                        }
2318                    })
2319                });
2320        }
2321    }
2322
2323    fn dismiss_notifications(&mut self, cx: &mut Context<Self>) {
2324        for window in self.notifications.drain(..) {
2325            window
2326                .update(cx, |_, window, _| {
2327                    window.remove_window();
2328                })
2329                .ok();
2330
2331            self.notification_subscriptions.remove(&window);
2332        }
2333    }
2334
2335    fn agent_ui_font_size_changed(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
2336        if let Some(entry_view_state) = self
2337            .active_thread()
2338            .map(|active| active.read(cx).entry_view_state.clone())
2339        {
2340            entry_view_state.update(cx, |entry_view_state, cx| {
2341                entry_view_state.agent_ui_font_size_changed(cx);
2342            });
2343        }
2344    }
2345
2346    pub(crate) fn insert_dragged_files(
2347        &self,
2348        paths: Vec<project::ProjectPath>,
2349        added_worktrees: Vec<Entity<project::Worktree>>,
2350        window: &mut Window,
2351        cx: &mut Context<Self>,
2352    ) {
2353        if let Some(active_thread) = self.active_thread() {
2354            active_thread.update(cx, |thread, cx| {
2355                thread.message_editor.update(cx, |editor, cx| {
2356                    editor.insert_dragged_files(paths, added_worktrees, window, cx);
2357                    editor.focus_handle(cx).focus(window, cx);
2358                })
2359            });
2360        }
2361    }
2362
2363    /// Inserts the selected text into the message editor or the message being
2364    /// edited, if any.
2365    pub(crate) fn insert_selections(&self, window: &mut Window, cx: &mut Context<Self>) {
2366        if let Some(active_thread) = self.active_thread() {
2367            active_thread.update(cx, |thread, cx| {
2368                thread.active_editor(cx).update(cx, |editor, cx| {
2369                    editor.insert_selections(window, cx);
2370                })
2371            });
2372        }
2373    }
2374
2375    /// Inserts terminal text as a crease into the message editor.
2376    pub(crate) fn insert_terminal_text(
2377        &self,
2378        text: String,
2379        window: &mut Window,
2380        cx: &mut Context<Self>,
2381    ) {
2382        if let Some(active_thread) = self.active_thread() {
2383            active_thread.update(cx, |thread, cx| {
2384                thread.message_editor.update(cx, |editor, cx| {
2385                    editor.insert_terminal_crease(text, window, cx);
2386                })
2387            });
2388        }
2389    }
2390
2391    fn current_model_name(&self, cx: &App) -> SharedString {
2392        // For native agent (Zed Agent), use the specific model name (e.g., "Claude 3.5 Sonnet")
2393        // For ACP agents, use the agent name (e.g., "Claude Code", "Gemini CLI")
2394        // This provides better clarity about what refused the request
2395        if self.as_native_connection(cx).is_some() {
2396            self.active_thread()
2397                .and_then(|active| active.read(cx).model_selector.clone())
2398                .and_then(|selector| selector.read(cx).active_model(cx))
2399                .map(|model| model.name.clone())
2400                .unwrap_or_else(|| SharedString::from("The model"))
2401        } else {
2402            // ACP agent - use the agent name (e.g., "Claude Code", "Gemini CLI")
2403            self.agent.name()
2404        }
2405    }
2406
2407    fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
2408        let message = message.into();
2409
2410        CopyButton::new("copy-error-message", message).tooltip_label("Copy Error Message")
2411    }
2412
2413    pub(crate) fn reauthenticate(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2414        let agent_name = self.agent.name();
2415        if let Some(active) = self.active_thread() {
2416            active.update(cx, |active, cx| active.clear_thread_error(cx));
2417        }
2418        let this = cx.weak_entity();
2419        window.defer(cx, |window, cx| {
2420            Self::handle_auth_required(this, AuthRequired::new(), agent_name, window, cx);
2421        })
2422    }
2423
2424    pub fn delete_history_entry(&mut self, entry: AgentSessionInfo, cx: &mut Context<Self>) {
2425        let task = self.history.update(cx, |history, cx| {
2426            history.delete_session(&entry.session_id, cx)
2427        });
2428        task.detach_and_log_err(cx);
2429    }
2430}
2431
2432fn loading_contents_spinner(size: IconSize) -> AnyElement {
2433    Icon::new(IconName::LoadCircle)
2434        .size(size)
2435        .color(Color::Accent)
2436        .with_rotate_animation(3)
2437        .into_any_element()
2438}
2439
2440fn placeholder_text(agent_name: &str, has_commands: bool) -> String {
2441    if agent_name == "Zed Agent" {
2442        format!("Message the {} — @ to include context", agent_name)
2443    } else if has_commands {
2444        format!(
2445            "Message {} — @ to include context, / for commands",
2446            agent_name
2447        )
2448    } else {
2449        format!("Message {} — @ to include context", agent_name)
2450    }
2451}
2452
2453impl Focusable for AcpServerView {
2454    fn focus_handle(&self, cx: &App) -> FocusHandle {
2455        match self.active_thread() {
2456            Some(thread) => thread.read(cx).focus_handle(cx),
2457            None => self.focus_handle.clone(),
2458        }
2459    }
2460}
2461
2462#[cfg(any(test, feature = "test-support"))]
2463impl AcpServerView {
2464    /// Expands a tool call so its content is visible.
2465    /// This is primarily useful for visual testing.
2466    pub fn expand_tool_call(&mut self, tool_call_id: acp::ToolCallId, cx: &mut Context<Self>) {
2467        if let Some(active) = self.active_thread() {
2468            active.update(cx, |active, _cx| {
2469                active.expanded_tool_calls.insert(tool_call_id);
2470            });
2471            cx.notify();
2472        }
2473    }
2474
2475    /// Expands a subagent card so its content is visible.
2476    /// This is primarily useful for visual testing.
2477    pub fn expand_subagent(&mut self, session_id: acp::SessionId, cx: &mut Context<Self>) {
2478        if let Some(active) = self.active_thread() {
2479            active.update(cx, |active, _cx| {
2480                active.expanded_subagents.insert(session_id);
2481            });
2482            cx.notify();
2483        }
2484    }
2485}
2486
2487impl Render for AcpServerView {
2488    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2489        self.sync_queued_message_editors(window, cx);
2490
2491        v_flex()
2492            .track_focus(&self.focus_handle)
2493            .size_full()
2494            .bg(cx.theme().colors().panel_background)
2495            .child(match &self.server_state {
2496                ServerState::Loading { .. } => v_flex()
2497                    .flex_1()
2498                    // .child(self.render_recent_history(cx))
2499                    .into_any(),
2500                ServerState::LoadError(e) => v_flex()
2501                    .flex_1()
2502                    .size_full()
2503                    .items_center()
2504                    .justify_end()
2505                    .child(self.render_load_error(e, window, cx))
2506                    .into_any(),
2507                ServerState::Connected(ConnectedServerState {
2508                    connection,
2509                    auth_state:
2510                        AuthState::Unauthenticated {
2511                            description,
2512                            configuration_view,
2513                            pending_auth_method,
2514                            _subscription,
2515                        },
2516                    ..
2517                }) => v_flex()
2518                    .flex_1()
2519                    .size_full()
2520                    .justify_end()
2521                    .child(self.render_auth_required_state(
2522                        connection,
2523                        description.as_ref(),
2524                        configuration_view.as_ref(),
2525                        pending_auth_method.as_ref(),
2526                        window,
2527                        cx,
2528                    ))
2529                    .into_any_element(),
2530                ServerState::Connected(connected) => connected.current.clone().into_any_element(),
2531            })
2532    }
2533}
2534
2535fn plan_label_markdown_style(
2536    status: &acp::PlanEntryStatus,
2537    window: &Window,
2538    cx: &App,
2539) -> MarkdownStyle {
2540    let default_md_style = MarkdownStyle::themed(MarkdownFont::Agent, window, cx);
2541
2542    MarkdownStyle {
2543        base_text_style: TextStyle {
2544            color: cx.theme().colors().text_muted,
2545            strikethrough: if matches!(status, acp::PlanEntryStatus::Completed) {
2546                Some(gpui::StrikethroughStyle {
2547                    thickness: px(1.),
2548                    color: Some(cx.theme().colors().text_muted.opacity(0.8)),
2549                })
2550            } else {
2551                None
2552            },
2553            ..default_md_style.base_text_style
2554        },
2555        ..default_md_style
2556    }
2557}
2558
2559#[cfg(test)]
2560pub(crate) mod tests {
2561    use acp_thread::{
2562        AgentSessionList, AgentSessionListRequest, AgentSessionListResponse, StubAgentConnection,
2563    };
2564    use action_log::ActionLog;
2565    use agent::{AgentTool, EditFileTool, FetchTool, TerminalTool, ToolPermissionContext};
2566    use agent_client_protocol::SessionId;
2567    use assistant_text_thread::TextThreadStore;
2568    use editor::MultiBufferOffset;
2569    use fs::FakeFs;
2570    use gpui::{EventEmitter, TestAppContext, VisualTestContext};
2571    use parking_lot::Mutex;
2572    use project::Project;
2573    use serde_json::json;
2574    use settings::SettingsStore;
2575    use std::any::Any;
2576    use std::path::{Path, PathBuf};
2577    use std::rc::Rc;
2578    use std::sync::Arc;
2579    use workspace::{Item, MultiWorkspace};
2580
2581    use crate::agent_panel;
2582
2583    use super::*;
2584
2585    #[gpui::test]
2586    async fn test_drop(cx: &mut TestAppContext) {
2587        init_test(cx);
2588
2589        let (thread_view, _cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
2590        let weak_view = thread_view.downgrade();
2591        drop(thread_view);
2592        assert!(!weak_view.is_upgradable());
2593    }
2594
2595    #[gpui::test]
2596    async fn test_notification_for_stop_event(cx: &mut TestAppContext) {
2597        init_test(cx);
2598
2599        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
2600
2601        let message_editor = message_editor(&thread_view, cx);
2602        message_editor.update_in(cx, |editor, window, cx| {
2603            editor.set_text("Hello", window, cx);
2604        });
2605
2606        cx.deactivate_window();
2607
2608        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
2609
2610        cx.run_until_parked();
2611
2612        assert!(
2613            cx.windows()
2614                .iter()
2615                .any(|window| window.downcast::<AgentNotification>().is_some())
2616        );
2617    }
2618
2619    #[gpui::test]
2620    async fn test_notification_for_error(cx: &mut TestAppContext) {
2621        init_test(cx);
2622
2623        let (thread_view, cx) =
2624            setup_thread_view(StubAgentServer::new(SaboteurAgentConnection), cx).await;
2625
2626        let message_editor = message_editor(&thread_view, cx);
2627        message_editor.update_in(cx, |editor, window, cx| {
2628            editor.set_text("Hello", window, cx);
2629        });
2630
2631        cx.deactivate_window();
2632
2633        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
2634
2635        cx.run_until_parked();
2636
2637        assert!(
2638            cx.windows()
2639                .iter()
2640                .any(|window| window.downcast::<AgentNotification>().is_some())
2641        );
2642    }
2643
2644    #[gpui::test]
2645    async fn test_recent_history_refreshes_when_history_cache_updated(cx: &mut TestAppContext) {
2646        init_test(cx);
2647
2648        let session_a = AgentSessionInfo::new(SessionId::new("session-a"));
2649        let session_b = AgentSessionInfo::new(SessionId::new("session-b"));
2650
2651        let fs = FakeFs::new(cx.executor());
2652        let project = Project::test(fs, [], cx).await;
2653        let (multi_workspace, cx) =
2654            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2655        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2656
2657        let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
2658        // Create history without an initial session list - it will be set after connection
2659        let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx)));
2660
2661        let thread_view = cx.update(|window, cx| {
2662            cx.new(|cx| {
2663                AcpServerView::new(
2664                    Rc::new(StubAgentServer::default_response()),
2665                    None,
2666                    None,
2667                    workspace.downgrade(),
2668                    project,
2669                    Some(thread_store),
2670                    None,
2671                    history.clone(),
2672                    window,
2673                    cx,
2674                )
2675            })
2676        });
2677
2678        // Wait for connection to establish
2679        cx.run_until_parked();
2680
2681        // Initially empty because StubAgentConnection.session_list() returns None
2682        active_thread(&thread_view, cx).read_with(cx, |view, _cx| {
2683            assert_eq!(view.recent_history_entries.len(), 0);
2684        });
2685
2686        // Now set the session list - this simulates external agents providing their history
2687        let list_a: Rc<dyn AgentSessionList> =
2688            Rc::new(StubSessionList::new(vec![session_a.clone()]));
2689        history.update(cx, |history, cx| {
2690            history.set_session_list(Some(list_a), cx);
2691        });
2692        cx.run_until_parked();
2693
2694        active_thread(&thread_view, cx).read_with(cx, |view, _cx| {
2695            assert_eq!(view.recent_history_entries.len(), 1);
2696            assert_eq!(
2697                view.recent_history_entries[0].session_id,
2698                session_a.session_id
2699            );
2700        });
2701
2702        // Update to a different session list
2703        let list_b: Rc<dyn AgentSessionList> =
2704            Rc::new(StubSessionList::new(vec![session_b.clone()]));
2705        history.update(cx, |history, cx| {
2706            history.set_session_list(Some(list_b), cx);
2707        });
2708        cx.run_until_parked();
2709
2710        active_thread(&thread_view, cx).read_with(cx, |view, _cx| {
2711            assert_eq!(view.recent_history_entries.len(), 1);
2712            assert_eq!(
2713                view.recent_history_entries[0].session_id,
2714                session_b.session_id
2715            );
2716        });
2717    }
2718
2719    #[gpui::test]
2720    async fn test_resume_without_history_adds_notice(cx: &mut TestAppContext) {
2721        init_test(cx);
2722
2723        let session = AgentSessionInfo::new(SessionId::new("resume-session"));
2724        let fs = FakeFs::new(cx.executor());
2725        let project = Project::test(fs, [], cx).await;
2726        let (multi_workspace, cx) =
2727            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2728        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2729
2730        let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
2731        let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx)));
2732
2733        let thread_view = cx.update(|window, cx| {
2734            cx.new(|cx| {
2735                AcpServerView::new(
2736                    Rc::new(StubAgentServer::new(ResumeOnlyAgentConnection)),
2737                    Some(session),
2738                    None,
2739                    workspace.downgrade(),
2740                    project,
2741                    Some(thread_store),
2742                    None,
2743                    history,
2744                    window,
2745                    cx,
2746                )
2747            })
2748        });
2749
2750        cx.run_until_parked();
2751
2752        thread_view.read_with(cx, |view, cx| {
2753            let state = view.active_thread().unwrap();
2754            assert!(state.read(cx).resumed_without_history);
2755            assert_eq!(state.read(cx).list_state.item_count(), 0);
2756        });
2757    }
2758
2759    #[gpui::test]
2760    async fn test_resume_thread_uses_session_cwd_when_inside_project(cx: &mut TestAppContext) {
2761        init_test(cx);
2762
2763        let fs = FakeFs::new(cx.executor());
2764        fs.insert_tree(
2765            "/project",
2766            json!({
2767                "subdir": {
2768                    "file.txt": "hello"
2769                }
2770            }),
2771        )
2772        .await;
2773        let project = Project::test(fs, [Path::new("/project")], cx).await;
2774        let (multi_workspace, cx) =
2775            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2776        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2777
2778        let connection = CwdCapturingConnection::new();
2779        let captured_cwd = connection.captured_cwd.clone();
2780
2781        let mut session = AgentSessionInfo::new(SessionId::new("session-1"));
2782        session.cwd = Some(PathBuf::from("/project/subdir"));
2783
2784        let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
2785        let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx)));
2786
2787        let _thread_view = cx.update(|window, cx| {
2788            cx.new(|cx| {
2789                AcpServerView::new(
2790                    Rc::new(StubAgentServer::new(connection)),
2791                    Some(session),
2792                    None,
2793                    workspace.downgrade(),
2794                    project,
2795                    Some(thread_store),
2796                    None,
2797                    history,
2798                    window,
2799                    cx,
2800                )
2801            })
2802        });
2803
2804        cx.run_until_parked();
2805
2806        assert_eq!(
2807            captured_cwd.lock().as_deref(),
2808            Some(Path::new("/project/subdir")),
2809            "Should use session cwd when it's inside the project"
2810        );
2811    }
2812
2813    #[gpui::test]
2814    async fn test_resume_thread_uses_fallback_cwd_when_outside_project(cx: &mut TestAppContext) {
2815        init_test(cx);
2816
2817        let fs = FakeFs::new(cx.executor());
2818        fs.insert_tree(
2819            "/project",
2820            json!({
2821                "file.txt": "hello"
2822            }),
2823        )
2824        .await;
2825        let project = Project::test(fs, [Path::new("/project")], cx).await;
2826        let (multi_workspace, cx) =
2827            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2828        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2829
2830        let connection = CwdCapturingConnection::new();
2831        let captured_cwd = connection.captured_cwd.clone();
2832
2833        let mut session = AgentSessionInfo::new(SessionId::new("session-1"));
2834        session.cwd = Some(PathBuf::from("/some/other/path"));
2835
2836        let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
2837        let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx)));
2838
2839        let _thread_view = cx.update(|window, cx| {
2840            cx.new(|cx| {
2841                AcpServerView::new(
2842                    Rc::new(StubAgentServer::new(connection)),
2843                    Some(session),
2844                    None,
2845                    workspace.downgrade(),
2846                    project,
2847                    Some(thread_store),
2848                    None,
2849                    history,
2850                    window,
2851                    cx,
2852                )
2853            })
2854        });
2855
2856        cx.run_until_parked();
2857
2858        assert_eq!(
2859            captured_cwd.lock().as_deref(),
2860            Some(Path::new("/project")),
2861            "Should use fallback project cwd when session cwd is outside the project"
2862        );
2863    }
2864
2865    #[gpui::test]
2866    async fn test_resume_thread_rejects_unnormalized_cwd_outside_project(cx: &mut TestAppContext) {
2867        init_test(cx);
2868
2869        let fs = FakeFs::new(cx.executor());
2870        fs.insert_tree(
2871            "/project",
2872            json!({
2873                "file.txt": "hello"
2874            }),
2875        )
2876        .await;
2877        let project = Project::test(fs, [Path::new("/project")], cx).await;
2878        let (multi_workspace, cx) =
2879            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
2880        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
2881
2882        let connection = CwdCapturingConnection::new();
2883        let captured_cwd = connection.captured_cwd.clone();
2884
2885        let mut session = AgentSessionInfo::new(SessionId::new("session-1"));
2886        session.cwd = Some(PathBuf::from("/project/../outside"));
2887
2888        let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
2889        let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx)));
2890
2891        let _thread_view = cx.update(|window, cx| {
2892            cx.new(|cx| {
2893                AcpServerView::new(
2894                    Rc::new(StubAgentServer::new(connection)),
2895                    Some(session),
2896                    None,
2897                    workspace.downgrade(),
2898                    project,
2899                    Some(thread_store),
2900                    None,
2901                    history,
2902                    window,
2903                    cx,
2904                )
2905            })
2906        });
2907
2908        cx.run_until_parked();
2909
2910        assert_eq!(
2911            captured_cwd.lock().as_deref(),
2912            Some(Path::new("/project")),
2913            "Should reject unnormalized cwd that resolves outside the project and use fallback cwd"
2914        );
2915    }
2916
2917    #[gpui::test]
2918    async fn test_refusal_handling(cx: &mut TestAppContext) {
2919        init_test(cx);
2920
2921        let (thread_view, cx) =
2922            setup_thread_view(StubAgentServer::new(RefusalAgentConnection), cx).await;
2923
2924        let message_editor = message_editor(&thread_view, cx);
2925        message_editor.update_in(cx, |editor, window, cx| {
2926            editor.set_text("Do something harmful", window, cx);
2927        });
2928
2929        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
2930
2931        cx.run_until_parked();
2932
2933        // Check that the refusal error is set
2934        thread_view.read_with(cx, |thread_view, cx| {
2935            let state = thread_view.active_thread().unwrap();
2936            assert!(
2937                matches!(state.read(cx).thread_error, Some(ThreadError::Refusal)),
2938                "Expected refusal error to be set"
2939            );
2940        });
2941    }
2942
2943    #[gpui::test]
2944    async fn test_notification_for_tool_authorization(cx: &mut TestAppContext) {
2945        init_test(cx);
2946
2947        let tool_call_id = acp::ToolCallId::new("1");
2948        let tool_call = acp::ToolCall::new(tool_call_id.clone(), "Label")
2949            .kind(acp::ToolKind::Edit)
2950            .content(vec!["hi".into()]);
2951        let connection =
2952            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
2953                tool_call_id,
2954                PermissionOptions::Flat(vec![acp::PermissionOption::new(
2955                    "1",
2956                    "Allow",
2957                    acp::PermissionOptionKind::AllowOnce,
2958                )]),
2959            )]));
2960
2961        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
2962
2963        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
2964
2965        let message_editor = message_editor(&thread_view, cx);
2966        message_editor.update_in(cx, |editor, window, cx| {
2967            editor.set_text("Hello", window, cx);
2968        });
2969
2970        cx.deactivate_window();
2971
2972        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
2973
2974        cx.run_until_parked();
2975
2976        assert!(
2977            cx.windows()
2978                .iter()
2979                .any(|window| window.downcast::<AgentNotification>().is_some())
2980        );
2981    }
2982
2983    #[gpui::test]
2984    async fn test_notification_when_panel_hidden(cx: &mut TestAppContext) {
2985        init_test(cx);
2986
2987        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
2988
2989        add_to_workspace(thread_view.clone(), cx);
2990
2991        let message_editor = message_editor(&thread_view, cx);
2992
2993        message_editor.update_in(cx, |editor, window, cx| {
2994            editor.set_text("Hello", window, cx);
2995        });
2996
2997        // Window is active (don't deactivate), but panel will be hidden
2998        // Note: In the test environment, the panel is not actually added to the dock,
2999        // so is_agent_panel_hidden will return true
3000
3001        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
3002
3003        cx.run_until_parked();
3004
3005        // Should show notification because window is active but panel is hidden
3006        assert!(
3007            cx.windows()
3008                .iter()
3009                .any(|window| window.downcast::<AgentNotification>().is_some()),
3010            "Expected notification when panel is hidden"
3011        );
3012    }
3013
3014    #[gpui::test]
3015    async fn test_notification_still_works_when_window_inactive(cx: &mut TestAppContext) {
3016        init_test(cx);
3017
3018        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
3019
3020        let message_editor = message_editor(&thread_view, cx);
3021        message_editor.update_in(cx, |editor, window, cx| {
3022            editor.set_text("Hello", window, cx);
3023        });
3024
3025        // Deactivate window - should show notification regardless of setting
3026        cx.deactivate_window();
3027
3028        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
3029
3030        cx.run_until_parked();
3031
3032        // Should still show notification when window is inactive (existing behavior)
3033        assert!(
3034            cx.windows()
3035                .iter()
3036                .any(|window| window.downcast::<AgentNotification>().is_some()),
3037            "Expected notification when window is inactive"
3038        );
3039    }
3040
3041    #[gpui::test]
3042    async fn test_notification_when_workspace_is_background_in_multi_workspace(
3043        cx: &mut TestAppContext,
3044    ) {
3045        init_test(cx);
3046
3047        // Enable multi-workspace feature flag and init globals needed by AgentPanel
3048        let fs = FakeFs::new(cx.executor());
3049
3050        cx.update(|cx| {
3051            cx.update_flags(true, vec!["agent-v2".to_string()]);
3052            agent::ThreadStore::init_global(cx);
3053            language_model::LanguageModelRegistry::test(cx);
3054            <dyn Fs>::set_global(fs.clone(), cx);
3055        });
3056
3057        let project1 = Project::test(fs.clone(), [], cx).await;
3058
3059        // Create a MultiWorkspace window with one workspace
3060        let multi_workspace_handle =
3061            cx.add_window(|window, cx| MultiWorkspace::test_new(project1.clone(), window, cx));
3062
3063        // Get workspace 1 (the initial workspace)
3064        let workspace1 = multi_workspace_handle
3065            .read_with(cx, |mw, _cx| mw.workspace().clone())
3066            .unwrap();
3067
3068        let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
3069
3070        workspace1.update_in(cx, |workspace, window, cx| {
3071            let text_thread_store =
3072                cx.new(|cx| TextThreadStore::fake(workspace.project().clone(), cx));
3073            let panel =
3074                cx.new(|cx| crate::AgentPanel::new(workspace, text_thread_store, None, window, cx));
3075            workspace.add_panel(panel, window, cx);
3076
3077            // Open the dock and activate the agent panel so it's visible
3078            workspace.focus_panel::<crate::AgentPanel>(window, cx);
3079        });
3080
3081        cx.run_until_parked();
3082
3083        cx.read(|cx| {
3084            assert!(
3085                crate::AgentPanel::is_visible(&workspace1, cx),
3086                "AgentPanel should be visible in workspace1's dock"
3087            );
3088        });
3089
3090        // Set up thread view in workspace 1
3091        let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
3092        let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx)));
3093
3094        let agent = StubAgentServer::default_response();
3095        let thread_view = cx.update(|window, cx| {
3096            cx.new(|cx| {
3097                AcpServerView::new(
3098                    Rc::new(agent),
3099                    None,
3100                    None,
3101                    workspace1.downgrade(),
3102                    project1.clone(),
3103                    Some(thread_store),
3104                    None,
3105                    history,
3106                    window,
3107                    cx,
3108                )
3109            })
3110        });
3111        cx.run_until_parked();
3112
3113        let message_editor = message_editor(&thread_view, cx);
3114        message_editor.update_in(cx, |editor, window, cx| {
3115            editor.set_text("Hello", window, cx);
3116        });
3117
3118        // Create a second workspace and switch to it.
3119        // This makes workspace1 the "background" workspace.
3120        let project2 = Project::test(fs, [], cx).await;
3121        multi_workspace_handle
3122            .update(cx, |mw, window, cx| {
3123                mw.test_add_workspace(project2, window, cx);
3124            })
3125            .unwrap();
3126
3127        cx.run_until_parked();
3128
3129        // Verify workspace1 is no longer the active workspace
3130        multi_workspace_handle
3131            .read_with(cx, |mw, _cx| {
3132                assert_eq!(mw.active_workspace_index(), 1);
3133                assert_ne!(mw.workspace(), &workspace1);
3134            })
3135            .unwrap();
3136
3137        // Window is active, agent panel is visible in workspace1, but workspace1
3138        // is in the background. The notification should show because the user
3139        // can't actually see the agent panel.
3140        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
3141
3142        cx.run_until_parked();
3143
3144        assert!(
3145            cx.windows()
3146                .iter()
3147                .any(|window| window.downcast::<AgentNotification>().is_some()),
3148            "Expected notification when workspace is in background within MultiWorkspace"
3149        );
3150
3151        // Also verify: clicking "View Panel" should switch to workspace1.
3152        cx.windows()
3153            .iter()
3154            .find_map(|window| window.downcast::<AgentNotification>())
3155            .unwrap()
3156            .update(cx, |window, _, cx| window.accept(cx))
3157            .unwrap();
3158
3159        cx.run_until_parked();
3160
3161        multi_workspace_handle
3162            .read_with(cx, |mw, _cx| {
3163                assert_eq!(
3164                    mw.workspace(),
3165                    &workspace1,
3166                    "Expected workspace1 to become the active workspace after accepting notification"
3167                );
3168            })
3169            .unwrap();
3170    }
3171
3172    #[gpui::test]
3173    async fn test_notification_respects_never_setting(cx: &mut TestAppContext) {
3174        init_test(cx);
3175
3176        // Set notify_when_agent_waiting to Never
3177        cx.update(|cx| {
3178            AgentSettings::override_global(
3179                AgentSettings {
3180                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
3181                    ..AgentSettings::get_global(cx).clone()
3182                },
3183                cx,
3184            );
3185        });
3186
3187        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
3188
3189        let message_editor = message_editor(&thread_view, cx);
3190        message_editor.update_in(cx, |editor, window, cx| {
3191            editor.set_text("Hello", window, cx);
3192        });
3193
3194        // Window is active
3195
3196        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
3197
3198        cx.run_until_parked();
3199
3200        // Should NOT show notification because notify_when_agent_waiting is Never
3201        assert!(
3202            !cx.windows()
3203                .iter()
3204                .any(|window| window.downcast::<AgentNotification>().is_some()),
3205            "Expected no notification when notify_when_agent_waiting is Never"
3206        );
3207    }
3208
3209    #[gpui::test]
3210    async fn test_notification_closed_when_thread_view_dropped(cx: &mut TestAppContext) {
3211        init_test(cx);
3212
3213        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
3214
3215        let weak_view = thread_view.downgrade();
3216
3217        let message_editor = message_editor(&thread_view, cx);
3218        message_editor.update_in(cx, |editor, window, cx| {
3219            editor.set_text("Hello", window, cx);
3220        });
3221
3222        cx.deactivate_window();
3223
3224        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
3225
3226        cx.run_until_parked();
3227
3228        // Verify notification is shown
3229        assert!(
3230            cx.windows()
3231                .iter()
3232                .any(|window| window.downcast::<AgentNotification>().is_some()),
3233            "Expected notification to be shown"
3234        );
3235
3236        // Drop the thread view (simulating navigation to a new thread)
3237        drop(thread_view);
3238        drop(message_editor);
3239        // Trigger an update to flush effects, which will call release_dropped_entities
3240        cx.update(|_window, _cx| {});
3241        cx.run_until_parked();
3242
3243        // Verify the entity was actually released
3244        assert!(
3245            !weak_view.is_upgradable(),
3246            "Thread view entity should be released after dropping"
3247        );
3248
3249        // The notification should be automatically closed via on_release
3250        assert!(
3251            !cx.windows()
3252                .iter()
3253                .any(|window| window.downcast::<AgentNotification>().is_some()),
3254            "Notification should be closed when thread view is dropped"
3255        );
3256    }
3257
3258    async fn setup_thread_view(
3259        agent: impl AgentServer + 'static,
3260        cx: &mut TestAppContext,
3261    ) -> (Entity<AcpServerView>, &mut VisualTestContext) {
3262        let fs = FakeFs::new(cx.executor());
3263        let project = Project::test(fs, [], cx).await;
3264        let (multi_workspace, cx) =
3265            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3266        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3267
3268        let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
3269        let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx)));
3270
3271        let thread_view = cx.update(|window, cx| {
3272            cx.new(|cx| {
3273                AcpServerView::new(
3274                    Rc::new(agent),
3275                    None,
3276                    None,
3277                    workspace.downgrade(),
3278                    project,
3279                    Some(thread_store),
3280                    None,
3281                    history,
3282                    window,
3283                    cx,
3284                )
3285            })
3286        });
3287        cx.run_until_parked();
3288        (thread_view, cx)
3289    }
3290
3291    fn add_to_workspace(thread_view: Entity<AcpServerView>, cx: &mut VisualTestContext) {
3292        let workspace = thread_view.read_with(cx, |thread_view, _cx| thread_view.workspace.clone());
3293
3294        workspace
3295            .update_in(cx, |workspace, window, cx| {
3296                workspace.add_item_to_active_pane(
3297                    Box::new(cx.new(|_| ThreadViewItem(thread_view.clone()))),
3298                    None,
3299                    true,
3300                    window,
3301                    cx,
3302                );
3303            })
3304            .unwrap();
3305    }
3306
3307    struct ThreadViewItem(Entity<AcpServerView>);
3308
3309    impl Item for ThreadViewItem {
3310        type Event = ();
3311
3312        fn include_in_nav_history() -> bool {
3313            false
3314        }
3315
3316        fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
3317            "Test".into()
3318        }
3319    }
3320
3321    impl EventEmitter<()> for ThreadViewItem {}
3322
3323    impl Focusable for ThreadViewItem {
3324        fn focus_handle(&self, cx: &App) -> FocusHandle {
3325            self.0.read(cx).focus_handle(cx)
3326        }
3327    }
3328
3329    impl Render for ThreadViewItem {
3330        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
3331            self.0.clone().into_any_element()
3332        }
3333    }
3334
3335    pub(crate) struct StubAgentServer<C> {
3336        connection: C,
3337    }
3338
3339    impl<C> StubAgentServer<C> {
3340        pub(crate) fn new(connection: C) -> Self {
3341            Self { connection }
3342        }
3343    }
3344
3345    impl StubAgentServer<StubAgentConnection> {
3346        pub(crate) fn default_response() -> Self {
3347            let conn = StubAgentConnection::new();
3348            conn.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
3349                acp::ContentChunk::new("Default response".into()),
3350            )]);
3351            Self::new(conn)
3352        }
3353    }
3354
3355    impl<C> AgentServer for StubAgentServer<C>
3356    where
3357        C: 'static + AgentConnection + Send + Clone,
3358    {
3359        fn logo(&self) -> ui::IconName {
3360            ui::IconName::Ai
3361        }
3362
3363        fn name(&self) -> SharedString {
3364            "Test".into()
3365        }
3366
3367        fn connect(
3368            &self,
3369            _root_dir: Option<&Path>,
3370            _delegate: AgentServerDelegate,
3371            _cx: &mut App,
3372        ) -> Task<gpui::Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
3373            Task::ready(Ok((Rc::new(self.connection.clone()), None)))
3374        }
3375
3376        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
3377            self
3378        }
3379    }
3380
3381    #[derive(Clone)]
3382    struct StubSessionList {
3383        sessions: Vec<AgentSessionInfo>,
3384    }
3385
3386    impl StubSessionList {
3387        fn new(sessions: Vec<AgentSessionInfo>) -> Self {
3388            Self { sessions }
3389        }
3390    }
3391
3392    impl AgentSessionList for StubSessionList {
3393        fn list_sessions(
3394            &self,
3395            _request: AgentSessionListRequest,
3396            _cx: &mut App,
3397        ) -> Task<anyhow::Result<AgentSessionListResponse>> {
3398            Task::ready(Ok(AgentSessionListResponse::new(self.sessions.clone())))
3399        }
3400        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
3401            self
3402        }
3403    }
3404
3405    #[derive(Clone)]
3406    struct ResumeOnlyAgentConnection;
3407
3408    impl AgentConnection for ResumeOnlyAgentConnection {
3409        fn telemetry_id(&self) -> SharedString {
3410            "resume-only".into()
3411        }
3412
3413        fn new_session(
3414            self: Rc<Self>,
3415            project: Entity<Project>,
3416            _cwd: &Path,
3417            cx: &mut gpui::App,
3418        ) -> Task<gpui::Result<Entity<AcpThread>>> {
3419            let action_log = cx.new(|_| ActionLog::new(project.clone()));
3420            let thread = cx.new(|cx| {
3421                AcpThread::new(
3422                    None,
3423                    "ResumeOnlyAgentConnection",
3424                    self.clone(),
3425                    project,
3426                    action_log,
3427                    SessionId::new("new-session"),
3428                    watch::Receiver::constant(
3429                        acp::PromptCapabilities::new()
3430                            .image(true)
3431                            .audio(true)
3432                            .embedded_context(true),
3433                    ),
3434                    cx,
3435                )
3436            });
3437            Task::ready(Ok(thread))
3438        }
3439
3440        fn supports_resume_session(&self, _cx: &App) -> bool {
3441            true
3442        }
3443
3444        fn resume_session(
3445            self: Rc<Self>,
3446            session: AgentSessionInfo,
3447            project: Entity<Project>,
3448            _cwd: &Path,
3449            cx: &mut App,
3450        ) -> Task<gpui::Result<Entity<AcpThread>>> {
3451            let action_log = cx.new(|_| ActionLog::new(project.clone()));
3452            let thread = cx.new(|cx| {
3453                AcpThread::new(
3454                    None,
3455                    "ResumeOnlyAgentConnection",
3456                    self.clone(),
3457                    project,
3458                    action_log,
3459                    session.session_id,
3460                    watch::Receiver::constant(
3461                        acp::PromptCapabilities::new()
3462                            .image(true)
3463                            .audio(true)
3464                            .embedded_context(true),
3465                    ),
3466                    cx,
3467                )
3468            });
3469            Task::ready(Ok(thread))
3470        }
3471
3472        fn auth_methods(&self) -> &[acp::AuthMethod] {
3473            &[]
3474        }
3475
3476        fn authenticate(
3477            &self,
3478            _method_id: acp::AuthMethodId,
3479            _cx: &mut App,
3480        ) -> Task<gpui::Result<()>> {
3481            Task::ready(Ok(()))
3482        }
3483
3484        fn prompt(
3485            &self,
3486            _id: Option<acp_thread::UserMessageId>,
3487            _params: acp::PromptRequest,
3488            _cx: &mut App,
3489        ) -> Task<gpui::Result<acp::PromptResponse>> {
3490            Task::ready(Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)))
3491        }
3492
3493        fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {}
3494
3495        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
3496            self
3497        }
3498    }
3499
3500    #[derive(Clone)]
3501    struct SaboteurAgentConnection;
3502
3503    impl AgentConnection for SaboteurAgentConnection {
3504        fn telemetry_id(&self) -> SharedString {
3505            "saboteur".into()
3506        }
3507
3508        fn new_session(
3509            self: Rc<Self>,
3510            project: Entity<Project>,
3511            _cwd: &Path,
3512            cx: &mut gpui::App,
3513        ) -> Task<gpui::Result<Entity<AcpThread>>> {
3514            Task::ready(Ok(cx.new(|cx| {
3515                let action_log = cx.new(|_| ActionLog::new(project.clone()));
3516                AcpThread::new(
3517                    None,
3518                    "SaboteurAgentConnection",
3519                    self,
3520                    project,
3521                    action_log,
3522                    SessionId::new("test"),
3523                    watch::Receiver::constant(
3524                        acp::PromptCapabilities::new()
3525                            .image(true)
3526                            .audio(true)
3527                            .embedded_context(true),
3528                    ),
3529                    cx,
3530                )
3531            })))
3532        }
3533
3534        fn auth_methods(&self) -> &[acp::AuthMethod] {
3535            &[]
3536        }
3537
3538        fn authenticate(
3539            &self,
3540            _method_id: acp::AuthMethodId,
3541            _cx: &mut App,
3542        ) -> Task<gpui::Result<()>> {
3543            unimplemented!()
3544        }
3545
3546        fn prompt(
3547            &self,
3548            _id: Option<acp_thread::UserMessageId>,
3549            _params: acp::PromptRequest,
3550            _cx: &mut App,
3551        ) -> Task<gpui::Result<acp::PromptResponse>> {
3552            Task::ready(Err(anyhow::anyhow!("Error prompting")))
3553        }
3554
3555        fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
3556            unimplemented!()
3557        }
3558
3559        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
3560            self
3561        }
3562    }
3563
3564    /// Simulates a model which always returns a refusal response
3565    #[derive(Clone)]
3566    struct RefusalAgentConnection;
3567
3568    impl AgentConnection for RefusalAgentConnection {
3569        fn telemetry_id(&self) -> SharedString {
3570            "refusal".into()
3571        }
3572
3573        fn new_session(
3574            self: Rc<Self>,
3575            project: Entity<Project>,
3576            _cwd: &Path,
3577            cx: &mut gpui::App,
3578        ) -> Task<gpui::Result<Entity<AcpThread>>> {
3579            Task::ready(Ok(cx.new(|cx| {
3580                let action_log = cx.new(|_| ActionLog::new(project.clone()));
3581                AcpThread::new(
3582                    None,
3583                    "RefusalAgentConnection",
3584                    self,
3585                    project,
3586                    action_log,
3587                    SessionId::new("test"),
3588                    watch::Receiver::constant(
3589                        acp::PromptCapabilities::new()
3590                            .image(true)
3591                            .audio(true)
3592                            .embedded_context(true),
3593                    ),
3594                    cx,
3595                )
3596            })))
3597        }
3598
3599        fn auth_methods(&self) -> &[acp::AuthMethod] {
3600            &[]
3601        }
3602
3603        fn authenticate(
3604            &self,
3605            _method_id: acp::AuthMethodId,
3606            _cx: &mut App,
3607        ) -> Task<gpui::Result<()>> {
3608            unimplemented!()
3609        }
3610
3611        fn prompt(
3612            &self,
3613            _id: Option<acp_thread::UserMessageId>,
3614            _params: acp::PromptRequest,
3615            _cx: &mut App,
3616        ) -> Task<gpui::Result<acp::PromptResponse>> {
3617            Task::ready(Ok(acp::PromptResponse::new(acp::StopReason::Refusal)))
3618        }
3619
3620        fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
3621            unimplemented!()
3622        }
3623
3624        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
3625            self
3626        }
3627    }
3628
3629    #[derive(Clone)]
3630    struct CwdCapturingConnection {
3631        captured_cwd: Arc<Mutex<Option<PathBuf>>>,
3632    }
3633
3634    impl CwdCapturingConnection {
3635        fn new() -> Self {
3636            Self {
3637                captured_cwd: Arc::new(Mutex::new(None)),
3638            }
3639        }
3640    }
3641
3642    impl AgentConnection for CwdCapturingConnection {
3643        fn telemetry_id(&self) -> SharedString {
3644            "cwd-capturing".into()
3645        }
3646
3647        fn new_session(
3648            self: Rc<Self>,
3649            project: Entity<Project>,
3650            cwd: &Path,
3651            cx: &mut gpui::App,
3652        ) -> Task<gpui::Result<Entity<AcpThread>>> {
3653            *self.captured_cwd.lock() = Some(cwd.to_path_buf());
3654            let action_log = cx.new(|_| ActionLog::new(project.clone()));
3655            let thread = cx.new(|cx| {
3656                AcpThread::new(
3657                    None,
3658                    "CwdCapturingConnection",
3659                    self.clone(),
3660                    project,
3661                    action_log,
3662                    SessionId::new("new-session"),
3663                    watch::Receiver::constant(
3664                        acp::PromptCapabilities::new()
3665                            .image(true)
3666                            .audio(true)
3667                            .embedded_context(true),
3668                    ),
3669                    cx,
3670                )
3671            });
3672            Task::ready(Ok(thread))
3673        }
3674
3675        fn supports_load_session(&self, _cx: &App) -> bool {
3676            true
3677        }
3678
3679        fn load_session(
3680            self: Rc<Self>,
3681            session: AgentSessionInfo,
3682            project: Entity<Project>,
3683            cwd: &Path,
3684            cx: &mut App,
3685        ) -> Task<gpui::Result<Entity<AcpThread>>> {
3686            *self.captured_cwd.lock() = Some(cwd.to_path_buf());
3687            let action_log = cx.new(|_| ActionLog::new(project.clone()));
3688            let thread = cx.new(|cx| {
3689                AcpThread::new(
3690                    None,
3691                    "CwdCapturingConnection",
3692                    self.clone(),
3693                    project,
3694                    action_log,
3695                    session.session_id,
3696                    watch::Receiver::constant(
3697                        acp::PromptCapabilities::new()
3698                            .image(true)
3699                            .audio(true)
3700                            .embedded_context(true),
3701                    ),
3702                    cx,
3703                )
3704            });
3705            Task::ready(Ok(thread))
3706        }
3707
3708        fn auth_methods(&self) -> &[acp::AuthMethod] {
3709            &[]
3710        }
3711
3712        fn authenticate(
3713            &self,
3714            _method_id: acp::AuthMethodId,
3715            _cx: &mut App,
3716        ) -> Task<gpui::Result<()>> {
3717            Task::ready(Ok(()))
3718        }
3719
3720        fn prompt(
3721            &self,
3722            _id: Option<acp_thread::UserMessageId>,
3723            _params: acp::PromptRequest,
3724            _cx: &mut App,
3725        ) -> Task<gpui::Result<acp::PromptResponse>> {
3726            Task::ready(Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)))
3727        }
3728
3729        fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {}
3730
3731        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
3732            self
3733        }
3734    }
3735
3736    pub(crate) fn init_test(cx: &mut TestAppContext) {
3737        cx.update(|cx| {
3738            let settings_store = SettingsStore::test(cx);
3739            cx.set_global(settings_store);
3740            theme::init(theme::LoadThemes::JustBase, cx);
3741            editor::init(cx);
3742            agent_panel::init(cx);
3743            release_channel::init(semver::Version::new(0, 0, 0), cx);
3744            prompt_store::init(cx)
3745        });
3746    }
3747
3748    fn active_thread(
3749        thread_view: &Entity<AcpServerView>,
3750        cx: &TestAppContext,
3751    ) -> Entity<AcpThreadView> {
3752        cx.read(|cx| thread_view.read(cx).as_connected().unwrap().current.clone())
3753    }
3754
3755    fn message_editor(
3756        thread_view: &Entity<AcpServerView>,
3757        cx: &TestAppContext,
3758    ) -> Entity<MessageEditor> {
3759        let thread = active_thread(thread_view, cx);
3760        cx.read(|cx| thread.read(cx).message_editor.clone())
3761    }
3762
3763    #[gpui::test]
3764    async fn test_rewind_views(cx: &mut TestAppContext) {
3765        init_test(cx);
3766
3767        let fs = FakeFs::new(cx.executor());
3768        fs.insert_tree(
3769            "/project",
3770            json!({
3771                "test1.txt": "old content 1",
3772                "test2.txt": "old content 2"
3773            }),
3774        )
3775        .await;
3776        let project = Project::test(fs, [Path::new("/project")], cx).await;
3777        let (multi_workspace, cx) =
3778            cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3779        let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3780
3781        let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
3782        let history = cx.update(|window, cx| cx.new(|cx| AcpThreadHistory::new(None, window, cx)));
3783
3784        let connection = Rc::new(StubAgentConnection::new());
3785        let thread_view = cx.update(|window, cx| {
3786            cx.new(|cx| {
3787                AcpServerView::new(
3788                    Rc::new(StubAgentServer::new(connection.as_ref().clone())),
3789                    None,
3790                    None,
3791                    workspace.downgrade(),
3792                    project.clone(),
3793                    Some(thread_store.clone()),
3794                    None,
3795                    history,
3796                    window,
3797                    cx,
3798                )
3799            })
3800        });
3801
3802        cx.run_until_parked();
3803
3804        let thread = thread_view
3805            .read_with(cx, |view, cx| {
3806                view.active_thread().map(|r| r.read(cx).thread.clone())
3807            })
3808            .unwrap();
3809
3810        // First user message
3811        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(
3812            acp::ToolCall::new("tool1", "Edit file 1")
3813                .kind(acp::ToolKind::Edit)
3814                .status(acp::ToolCallStatus::Completed)
3815                .content(vec![acp::ToolCallContent::Diff(
3816                    acp::Diff::new("/project/test1.txt", "new content 1").old_text("old content 1"),
3817                )]),
3818        )]);
3819
3820        thread
3821            .update(cx, |thread, cx| thread.send_raw("Give me a diff", cx))
3822            .await
3823            .unwrap();
3824        cx.run_until_parked();
3825
3826        thread.read_with(cx, |thread, _cx| {
3827            assert_eq!(thread.entries().len(), 2);
3828        });
3829
3830        thread_view.read_with(cx, |view, cx| {
3831            let entry_view_state = view
3832                .active_thread()
3833                .map(|active| active.read(cx).entry_view_state.clone())
3834                .unwrap();
3835            entry_view_state.read_with(cx, |entry_view_state, _| {
3836                assert!(
3837                    entry_view_state
3838                        .entry(0)
3839                        .unwrap()
3840                        .message_editor()
3841                        .is_some()
3842                );
3843                assert!(entry_view_state.entry(1).unwrap().has_content());
3844            });
3845        });
3846
3847        // Second user message
3848        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(
3849            acp::ToolCall::new("tool2", "Edit file 2")
3850                .kind(acp::ToolKind::Edit)
3851                .status(acp::ToolCallStatus::Completed)
3852                .content(vec![acp::ToolCallContent::Diff(
3853                    acp::Diff::new("/project/test2.txt", "new content 2").old_text("old content 2"),
3854                )]),
3855        )]);
3856
3857        thread
3858            .update(cx, |thread, cx| thread.send_raw("Another one", cx))
3859            .await
3860            .unwrap();
3861        cx.run_until_parked();
3862
3863        let second_user_message_id = thread.read_with(cx, |thread, _| {
3864            assert_eq!(thread.entries().len(), 4);
3865            let AgentThreadEntry::UserMessage(user_message) = &thread.entries()[2] else {
3866                panic!();
3867            };
3868            user_message.id.clone().unwrap()
3869        });
3870
3871        thread_view.read_with(cx, |view, cx| {
3872            let entry_view_state = view
3873                .active_thread()
3874                .unwrap()
3875                .read(cx)
3876                .entry_view_state
3877                .clone();
3878            entry_view_state.read_with(cx, |entry_view_state, _| {
3879                assert!(
3880                    entry_view_state
3881                        .entry(0)
3882                        .unwrap()
3883                        .message_editor()
3884                        .is_some()
3885                );
3886                assert!(entry_view_state.entry(1).unwrap().has_content());
3887                assert!(
3888                    entry_view_state
3889                        .entry(2)
3890                        .unwrap()
3891                        .message_editor()
3892                        .is_some()
3893                );
3894                assert!(entry_view_state.entry(3).unwrap().has_content());
3895            });
3896        });
3897
3898        // Rewind to first message
3899        thread
3900            .update(cx, |thread, cx| thread.rewind(second_user_message_id, cx))
3901            .await
3902            .unwrap();
3903
3904        cx.run_until_parked();
3905
3906        thread.read_with(cx, |thread, _| {
3907            assert_eq!(thread.entries().len(), 2);
3908        });
3909
3910        thread_view.read_with(cx, |view, cx| {
3911            let active = view.active_thread().unwrap();
3912            active
3913                .read(cx)
3914                .entry_view_state
3915                .read_with(cx, |entry_view_state, _| {
3916                    assert!(
3917                        entry_view_state
3918                            .entry(0)
3919                            .unwrap()
3920                            .message_editor()
3921                            .is_some()
3922                    );
3923                    assert!(entry_view_state.entry(1).unwrap().has_content());
3924
3925                    // Old views should be dropped
3926                    assert!(entry_view_state.entry(2).is_none());
3927                    assert!(entry_view_state.entry(3).is_none());
3928                });
3929        });
3930    }
3931
3932    #[gpui::test]
3933    async fn test_scroll_to_most_recent_user_prompt(cx: &mut TestAppContext) {
3934        init_test(cx);
3935
3936        let connection = StubAgentConnection::new();
3937
3938        // Each user prompt will result in a user message entry plus an agent message entry.
3939        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
3940            acp::ContentChunk::new("Response 1".into()),
3941        )]);
3942
3943        let (thread_view, cx) =
3944            setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
3945
3946        let thread = thread_view
3947            .read_with(cx, |view, cx| {
3948                view.active_thread().map(|r| r.read(cx).thread.clone())
3949            })
3950            .unwrap();
3951
3952        thread
3953            .update(cx, |thread, cx| thread.send_raw("Prompt 1", cx))
3954            .await
3955            .unwrap();
3956        cx.run_until_parked();
3957
3958        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
3959            acp::ContentChunk::new("Response 2".into()),
3960        )]);
3961
3962        thread
3963            .update(cx, |thread, cx| thread.send_raw("Prompt 2", cx))
3964            .await
3965            .unwrap();
3966        cx.run_until_parked();
3967
3968        // Move somewhere else first so we're not trivially already on the last user prompt.
3969        active_thread(&thread_view, cx).update(cx, |view, cx| {
3970            view.scroll_to_top(cx);
3971        });
3972        cx.run_until_parked();
3973
3974        active_thread(&thread_view, cx).update(cx, |view, cx| {
3975            view.scroll_to_most_recent_user_prompt(cx);
3976            let scroll_top = view.list_state.logical_scroll_top();
3977            // Entries layout is: [User1, Assistant1, User2, Assistant2]
3978            assert_eq!(scroll_top.item_ix, 2);
3979        });
3980    }
3981
3982    #[gpui::test]
3983    async fn test_scroll_to_most_recent_user_prompt_falls_back_to_bottom_without_user_messages(
3984        cx: &mut TestAppContext,
3985    ) {
3986        init_test(cx);
3987
3988        let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await;
3989
3990        // With no entries, scrolling should be a no-op and must not panic.
3991        active_thread(&thread_view, cx).update(cx, |view, cx| {
3992            view.scroll_to_most_recent_user_prompt(cx);
3993            let scroll_top = view.list_state.logical_scroll_top();
3994            assert_eq!(scroll_top.item_ix, 0);
3995        });
3996    }
3997
3998    #[gpui::test]
3999    async fn test_message_editing_cancel(cx: &mut TestAppContext) {
4000        init_test(cx);
4001
4002        let connection = StubAgentConnection::new();
4003
4004        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
4005            acp::ContentChunk::new("Response".into()),
4006        )]);
4007
4008        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
4009        add_to_workspace(thread_view.clone(), cx);
4010
4011        let message_editor = message_editor(&thread_view, cx);
4012        message_editor.update_in(cx, |editor, window, cx| {
4013            editor.set_text("Original message to edit", window, cx);
4014        });
4015        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
4016
4017        cx.run_until_parked();
4018
4019        let user_message_editor = thread_view.read_with(cx, |view, cx| {
4020            assert_eq!(
4021                view.active_thread()
4022                    .and_then(|active| active.read(cx).editing_message),
4023                None
4024            );
4025
4026            view.active_thread()
4027                .map(|active| &active.read(cx).entry_view_state)
4028                .as_ref()
4029                .unwrap()
4030                .read(cx)
4031                .entry(0)
4032                .unwrap()
4033                .message_editor()
4034                .unwrap()
4035                .clone()
4036        });
4037
4038        // Focus
4039        cx.focus(&user_message_editor);
4040        thread_view.read_with(cx, |view, cx| {
4041            assert_eq!(
4042                view.active_thread()
4043                    .and_then(|active| active.read(cx).editing_message),
4044                Some(0)
4045            );
4046        });
4047
4048        // Edit
4049        user_message_editor.update_in(cx, |editor, window, cx| {
4050            editor.set_text("Edited message content", window, cx);
4051        });
4052
4053        // Cancel
4054        user_message_editor.update_in(cx, |_editor, window, cx| {
4055            window.dispatch_action(Box::new(editor::actions::Cancel), cx);
4056        });
4057
4058        thread_view.read_with(cx, |view, cx| {
4059            assert_eq!(
4060                view.active_thread()
4061                    .and_then(|active| active.read(cx).editing_message),
4062                None
4063            );
4064        });
4065
4066        user_message_editor.read_with(cx, |editor, cx| {
4067            assert_eq!(editor.text(cx), "Original message to edit");
4068        });
4069    }
4070
4071    #[gpui::test]
4072    async fn test_message_doesnt_send_if_empty(cx: &mut TestAppContext) {
4073        init_test(cx);
4074
4075        let connection = StubAgentConnection::new();
4076
4077        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
4078        add_to_workspace(thread_view.clone(), cx);
4079
4080        let message_editor = message_editor(&thread_view, cx);
4081        message_editor.update_in(cx, |editor, window, cx| {
4082            editor.set_text("", window, cx);
4083        });
4084
4085        let thread = cx.read(|cx| {
4086            thread_view
4087                .read(cx)
4088                .active_thread()
4089                .unwrap()
4090                .read(cx)
4091                .thread
4092                .clone()
4093        });
4094        let entries_before = cx.read(|cx| thread.read(cx).entries().len());
4095
4096        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| {
4097            view.send(window, cx);
4098        });
4099        cx.run_until_parked();
4100
4101        let entries_after = cx.read(|cx| thread.read(cx).entries().len());
4102        assert_eq!(
4103            entries_before, entries_after,
4104            "No message should be sent when editor is empty"
4105        );
4106    }
4107
4108    #[gpui::test]
4109    async fn test_message_editing_regenerate(cx: &mut TestAppContext) {
4110        init_test(cx);
4111
4112        let connection = StubAgentConnection::new();
4113
4114        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
4115            acp::ContentChunk::new("Response".into()),
4116        )]);
4117
4118        let (thread_view, cx) =
4119            setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
4120        add_to_workspace(thread_view.clone(), cx);
4121
4122        let message_editor = message_editor(&thread_view, cx);
4123        message_editor.update_in(cx, |editor, window, cx| {
4124            editor.set_text("Original message to edit", window, cx);
4125        });
4126        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
4127
4128        cx.run_until_parked();
4129
4130        let user_message_editor = thread_view.read_with(cx, |view, cx| {
4131            assert_eq!(
4132                view.active_thread()
4133                    .and_then(|active| active.read(cx).editing_message),
4134                None
4135            );
4136            assert_eq!(
4137                view.active_thread()
4138                    .unwrap()
4139                    .read(cx)
4140                    .thread
4141                    .read(cx)
4142                    .entries()
4143                    .len(),
4144                2
4145            );
4146
4147            view.active_thread()
4148                .map(|active| &active.read(cx).entry_view_state)
4149                .as_ref()
4150                .unwrap()
4151                .read(cx)
4152                .entry(0)
4153                .unwrap()
4154                .message_editor()
4155                .unwrap()
4156                .clone()
4157        });
4158
4159        // Focus
4160        cx.focus(&user_message_editor);
4161
4162        // Edit
4163        user_message_editor.update_in(cx, |editor, window, cx| {
4164            editor.set_text("Edited message content", window, cx);
4165        });
4166
4167        // Send
4168        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
4169            acp::ContentChunk::new("New Response".into()),
4170        )]);
4171
4172        user_message_editor.update_in(cx, |_editor, window, cx| {
4173            window.dispatch_action(Box::new(Chat), cx);
4174        });
4175
4176        cx.run_until_parked();
4177
4178        thread_view.read_with(cx, |view, cx| {
4179            assert_eq!(
4180                view.active_thread()
4181                    .and_then(|active| active.read(cx).editing_message),
4182                None
4183            );
4184
4185            let entries = view
4186                .active_thread()
4187                .unwrap()
4188                .read(cx)
4189                .thread
4190                .read(cx)
4191                .entries();
4192            assert_eq!(entries.len(), 2);
4193            assert_eq!(
4194                entries[0].to_markdown(cx),
4195                "## User\n\nEdited message content\n\n"
4196            );
4197            assert_eq!(
4198                entries[1].to_markdown(cx),
4199                "## Assistant\n\nNew Response\n\n"
4200            );
4201
4202            let entry_view_state = view
4203                .active_thread()
4204                .map(|active| &active.read(cx).entry_view_state)
4205                .unwrap();
4206            let new_editor = entry_view_state.read_with(cx, |state, _cx| {
4207                assert!(!state.entry(1).unwrap().has_content());
4208                state.entry(0).unwrap().message_editor().unwrap().clone()
4209            });
4210
4211            assert_eq!(new_editor.read(cx).text(cx), "Edited message content");
4212        })
4213    }
4214
4215    #[gpui::test]
4216    async fn test_message_editing_while_generating(cx: &mut TestAppContext) {
4217        init_test(cx);
4218
4219        let connection = StubAgentConnection::new();
4220
4221        let (thread_view, cx) =
4222            setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
4223        add_to_workspace(thread_view.clone(), cx);
4224
4225        let message_editor = message_editor(&thread_view, cx);
4226        message_editor.update_in(cx, |editor, window, cx| {
4227            editor.set_text("Original message to edit", window, cx);
4228        });
4229        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
4230
4231        cx.run_until_parked();
4232
4233        let (user_message_editor, session_id) = thread_view.read_with(cx, |view, cx| {
4234            let thread = view.active_thread().unwrap().read(cx).thread.read(cx);
4235            assert_eq!(thread.entries().len(), 1);
4236
4237            let editor = view
4238                .active_thread()
4239                .map(|active| &active.read(cx).entry_view_state)
4240                .as_ref()
4241                .unwrap()
4242                .read(cx)
4243                .entry(0)
4244                .unwrap()
4245                .message_editor()
4246                .unwrap()
4247                .clone();
4248
4249            (editor, thread.session_id().clone())
4250        });
4251
4252        // Focus
4253        cx.focus(&user_message_editor);
4254
4255        thread_view.read_with(cx, |view, cx| {
4256            assert_eq!(
4257                view.active_thread()
4258                    .and_then(|active| active.read(cx).editing_message),
4259                Some(0)
4260            );
4261        });
4262
4263        // Edit
4264        user_message_editor.update_in(cx, |editor, window, cx| {
4265            editor.set_text("Edited message content", window, cx);
4266        });
4267
4268        thread_view.read_with(cx, |view, cx| {
4269            assert_eq!(
4270                view.active_thread()
4271                    .and_then(|active| active.read(cx).editing_message),
4272                Some(0)
4273            );
4274        });
4275
4276        // Finish streaming response
4277        cx.update(|_, cx| {
4278            connection.send_update(
4279                session_id.clone(),
4280                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("Response".into())),
4281                cx,
4282            );
4283            connection.end_turn(session_id, acp::StopReason::EndTurn);
4284        });
4285
4286        thread_view.read_with(cx, |view, cx| {
4287            assert_eq!(
4288                view.active_thread()
4289                    .and_then(|active| active.read(cx).editing_message),
4290                Some(0)
4291            );
4292        });
4293
4294        cx.run_until_parked();
4295
4296        // Should still be editing
4297        cx.update(|window, cx| {
4298            assert!(user_message_editor.focus_handle(cx).is_focused(window));
4299            assert_eq!(
4300                thread_view
4301                    .read(cx)
4302                    .active_thread()
4303                    .and_then(|active| active.read(cx).editing_message),
4304                Some(0)
4305            );
4306            assert_eq!(
4307                user_message_editor.read(cx).text(cx),
4308                "Edited message content"
4309            );
4310        });
4311    }
4312
4313    struct GeneratingThreadSetup {
4314        thread_view: Entity<AcpServerView>,
4315        thread: Entity<AcpThread>,
4316        message_editor: Entity<MessageEditor>,
4317    }
4318
4319    async fn setup_generating_thread(
4320        cx: &mut TestAppContext,
4321    ) -> (GeneratingThreadSetup, &mut VisualTestContext) {
4322        let connection = StubAgentConnection::new();
4323
4324        let (thread_view, cx) =
4325            setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
4326        add_to_workspace(thread_view.clone(), cx);
4327
4328        let message_editor = message_editor(&thread_view, cx);
4329        message_editor.update_in(cx, |editor, window, cx| {
4330            editor.set_text("Hello", window, cx);
4331        });
4332        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
4333
4334        let (thread, session_id) = thread_view.read_with(cx, |view, cx| {
4335            let thread = view
4336                .active_thread()
4337                .as_ref()
4338                .unwrap()
4339                .read(cx)
4340                .thread
4341                .clone();
4342            (thread.clone(), thread.read(cx).session_id().clone())
4343        });
4344
4345        cx.run_until_parked();
4346
4347        cx.update(|_, cx| {
4348            connection.send_update(
4349                session_id.clone(),
4350                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
4351                    "Response chunk".into(),
4352                )),
4353                cx,
4354            );
4355        });
4356
4357        cx.run_until_parked();
4358
4359        thread.read_with(cx, |thread, _cx| {
4360            assert_eq!(thread.status(), ThreadStatus::Generating);
4361        });
4362
4363        (
4364            GeneratingThreadSetup {
4365                thread_view,
4366                thread,
4367                message_editor,
4368            },
4369            cx,
4370        )
4371    }
4372
4373    #[gpui::test]
4374    async fn test_escape_cancels_generation_from_conversation_focus(cx: &mut TestAppContext) {
4375        init_test(cx);
4376
4377        let (setup, cx) = setup_generating_thread(cx).await;
4378
4379        let focus_handle = setup
4380            .thread_view
4381            .read_with(cx, |view, cx| view.focus_handle(cx));
4382        cx.update(|window, cx| {
4383            window.focus(&focus_handle, cx);
4384        });
4385
4386        setup.thread_view.update_in(cx, |_, window, cx| {
4387            window.dispatch_action(menu::Cancel.boxed_clone(), cx);
4388        });
4389
4390        cx.run_until_parked();
4391
4392        setup.thread.read_with(cx, |thread, _cx| {
4393            assert_eq!(thread.status(), ThreadStatus::Idle);
4394        });
4395    }
4396
4397    #[gpui::test]
4398    async fn test_escape_cancels_generation_from_editor_focus(cx: &mut TestAppContext) {
4399        init_test(cx);
4400
4401        let (setup, cx) = setup_generating_thread(cx).await;
4402
4403        let editor_focus_handle = setup
4404            .message_editor
4405            .read_with(cx, |editor, cx| editor.focus_handle(cx));
4406        cx.update(|window, cx| {
4407            window.focus(&editor_focus_handle, cx);
4408        });
4409
4410        setup.message_editor.update_in(cx, |_, window, cx| {
4411            window.dispatch_action(editor::actions::Cancel.boxed_clone(), cx);
4412        });
4413
4414        cx.run_until_parked();
4415
4416        setup.thread.read_with(cx, |thread, _cx| {
4417            assert_eq!(thread.status(), ThreadStatus::Idle);
4418        });
4419    }
4420
4421    #[gpui::test]
4422    async fn test_escape_when_idle_is_noop(cx: &mut TestAppContext) {
4423        init_test(cx);
4424
4425        let (thread_view, cx) =
4426            setup_thread_view(StubAgentServer::new(StubAgentConnection::new()), cx).await;
4427        add_to_workspace(thread_view.clone(), cx);
4428
4429        let thread = thread_view.read_with(cx, |view, cx| {
4430            view.active_thread().unwrap().read(cx).thread.clone()
4431        });
4432
4433        thread.read_with(cx, |thread, _cx| {
4434            assert_eq!(thread.status(), ThreadStatus::Idle);
4435        });
4436
4437        let focus_handle = thread_view.read_with(cx, |view, _cx| view.focus_handle.clone());
4438        cx.update(|window, cx| {
4439            window.focus(&focus_handle, cx);
4440        });
4441
4442        thread_view.update_in(cx, |_, window, cx| {
4443            window.dispatch_action(menu::Cancel.boxed_clone(), cx);
4444        });
4445
4446        cx.run_until_parked();
4447
4448        thread.read_with(cx, |thread, _cx| {
4449            assert_eq!(thread.status(), ThreadStatus::Idle);
4450        });
4451    }
4452
4453    #[gpui::test]
4454    async fn test_interrupt(cx: &mut TestAppContext) {
4455        init_test(cx);
4456
4457        let connection = StubAgentConnection::new();
4458
4459        let (thread_view, cx) =
4460            setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
4461        add_to_workspace(thread_view.clone(), cx);
4462
4463        let message_editor = message_editor(&thread_view, cx);
4464        message_editor.update_in(cx, |editor, window, cx| {
4465            editor.set_text("Message 1", window, cx);
4466        });
4467        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
4468
4469        let (thread, session_id) = thread_view.read_with(cx, |view, cx| {
4470            let thread = view.active_thread().unwrap().read(cx).thread.clone();
4471
4472            (thread.clone(), thread.read(cx).session_id().clone())
4473        });
4474
4475        cx.run_until_parked();
4476
4477        cx.update(|_, cx| {
4478            connection.send_update(
4479                session_id.clone(),
4480                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
4481                    "Message 1 resp".into(),
4482                )),
4483                cx,
4484            );
4485        });
4486
4487        cx.run_until_parked();
4488
4489        thread.read_with(cx, |thread, cx| {
4490            assert_eq!(
4491                thread.to_markdown(cx),
4492                indoc::indoc! {"
4493                        ## User
4494
4495                        Message 1
4496
4497                        ## Assistant
4498
4499                        Message 1 resp
4500
4501                    "}
4502            )
4503        });
4504
4505        message_editor.update_in(cx, |editor, window, cx| {
4506            editor.set_text("Message 2", window, cx);
4507        });
4508        active_thread(&thread_view, cx)
4509            .update_in(cx, |view, window, cx| view.interrupt_and_send(window, cx));
4510
4511        cx.update(|_, cx| {
4512            // Simulate a response sent after beginning to cancel
4513            connection.send_update(
4514                session_id.clone(),
4515                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("onse".into())),
4516                cx,
4517            );
4518        });
4519
4520        cx.run_until_parked();
4521
4522        // Last Message 1 response should appear before Message 2
4523        thread.read_with(cx, |thread, cx| {
4524            assert_eq!(
4525                thread.to_markdown(cx),
4526                indoc::indoc! {"
4527                        ## User
4528
4529                        Message 1
4530
4531                        ## Assistant
4532
4533                        Message 1 response
4534
4535                        ## User
4536
4537                        Message 2
4538
4539                    "}
4540            )
4541        });
4542
4543        cx.update(|_, cx| {
4544            connection.send_update(
4545                session_id.clone(),
4546                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
4547                    "Message 2 response".into(),
4548                )),
4549                cx,
4550            );
4551            connection.end_turn(session_id.clone(), acp::StopReason::EndTurn);
4552        });
4553
4554        cx.run_until_parked();
4555
4556        thread.read_with(cx, |thread, cx| {
4557            assert_eq!(
4558                thread.to_markdown(cx),
4559                indoc::indoc! {"
4560                        ## User
4561
4562                        Message 1
4563
4564                        ## Assistant
4565
4566                        Message 1 response
4567
4568                        ## User
4569
4570                        Message 2
4571
4572                        ## Assistant
4573
4574                        Message 2 response
4575
4576                    "}
4577            )
4578        });
4579    }
4580
4581    #[gpui::test]
4582    async fn test_message_editing_insert_selections(cx: &mut TestAppContext) {
4583        init_test(cx);
4584
4585        let connection = StubAgentConnection::new();
4586        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
4587            acp::ContentChunk::new("Response".into()),
4588        )]);
4589
4590        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
4591        add_to_workspace(thread_view.clone(), cx);
4592
4593        let message_editor = message_editor(&thread_view, cx);
4594        message_editor.update_in(cx, |editor, window, cx| {
4595            editor.set_text("Original message to edit", window, cx)
4596        });
4597        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
4598        cx.run_until_parked();
4599
4600        let user_message_editor = thread_view.read_with(cx, |thread_view, cx| {
4601            thread_view
4602                .active_thread()
4603                .map(|active| &active.read(cx).entry_view_state)
4604                .as_ref()
4605                .unwrap()
4606                .read(cx)
4607                .entry(0)
4608                .expect("Should have at least one entry")
4609                .message_editor()
4610                .expect("Should have message editor")
4611                .clone()
4612        });
4613
4614        cx.focus(&user_message_editor);
4615        thread_view.read_with(cx, |view, cx| {
4616            assert_eq!(
4617                view.active_thread()
4618                    .and_then(|active| active.read(cx).editing_message),
4619                Some(0)
4620            );
4621        });
4622
4623        // Ensure to edit the focused message before proceeding otherwise, since
4624        // its content is not different from what was sent, focus will be lost.
4625        user_message_editor.update_in(cx, |editor, window, cx| {
4626            editor.set_text("Original message to edit with ", window, cx)
4627        });
4628
4629        // Create a simple buffer with some text so we can create a selection
4630        // that will then be added to the message being edited.
4631        let (workspace, project) = thread_view.read_with(cx, |thread_view, _cx| {
4632            (thread_view.workspace.clone(), thread_view.project.clone())
4633        });
4634        let buffer = project.update(cx, |project, cx| {
4635            project.create_local_buffer("let a = 10 + 10;", None, false, cx)
4636        });
4637
4638        workspace
4639            .update_in(cx, |workspace, window, cx| {
4640                let editor = cx.new(|cx| {
4641                    let mut editor =
4642                        Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx);
4643
4644                    editor.change_selections(Default::default(), window, cx, |selections| {
4645                        selections.select_ranges([MultiBufferOffset(8)..MultiBufferOffset(15)]);
4646                    });
4647
4648                    editor
4649                });
4650                workspace.add_item_to_active_pane(Box::new(editor), None, false, window, cx);
4651            })
4652            .unwrap();
4653
4654        thread_view.update_in(cx, |view, window, cx| {
4655            assert_eq!(
4656                view.active_thread()
4657                    .and_then(|active| active.read(cx).editing_message),
4658                Some(0)
4659            );
4660            view.insert_selections(window, cx);
4661        });
4662
4663        user_message_editor.read_with(cx, |editor, cx| {
4664            let text = editor.editor().read(cx).text(cx);
4665            let expected_text = String::from("Original message to edit with selection ");
4666
4667            assert_eq!(text, expected_text);
4668        });
4669    }
4670
4671    #[gpui::test]
4672    async fn test_insert_selections(cx: &mut TestAppContext) {
4673        init_test(cx);
4674
4675        let connection = StubAgentConnection::new();
4676        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
4677            acp::ContentChunk::new("Response".into()),
4678        )]);
4679
4680        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
4681        add_to_workspace(thread_view.clone(), cx);
4682
4683        let message_editor = message_editor(&thread_view, cx);
4684        message_editor.update_in(cx, |editor, window, cx| {
4685            editor.set_text("Can you review this snippet ", window, cx)
4686        });
4687
4688        // Create a simple buffer with some text so we can create a selection
4689        // that will then be added to the message being edited.
4690        let (workspace, project) = thread_view.read_with(cx, |thread_view, _cx| {
4691            (thread_view.workspace.clone(), thread_view.project.clone())
4692        });
4693        let buffer = project.update(cx, |project, cx| {
4694            project.create_local_buffer("let a = 10 + 10;", None, false, cx)
4695        });
4696
4697        workspace
4698            .update_in(cx, |workspace, window, cx| {
4699                let editor = cx.new(|cx| {
4700                    let mut editor =
4701                        Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx);
4702
4703                    editor.change_selections(Default::default(), window, cx, |selections| {
4704                        selections.select_ranges([MultiBufferOffset(8)..MultiBufferOffset(15)]);
4705                    });
4706
4707                    editor
4708                });
4709                workspace.add_item_to_active_pane(Box::new(editor), None, false, window, cx);
4710            })
4711            .unwrap();
4712
4713        thread_view.update_in(cx, |view, window, cx| {
4714            assert_eq!(
4715                view.active_thread()
4716                    .and_then(|active| active.read(cx).editing_message),
4717                None
4718            );
4719            view.insert_selections(window, cx);
4720        });
4721
4722        message_editor.read_with(cx, |editor, cx| {
4723            let text = editor.text(cx);
4724            let expected_txt = String::from("Can you review this snippet selection ");
4725
4726            assert_eq!(text, expected_txt);
4727        })
4728    }
4729
4730    #[gpui::test]
4731    async fn test_tool_permission_buttons_terminal_with_pattern(cx: &mut TestAppContext) {
4732        init_test(cx);
4733
4734        let tool_call_id = acp::ToolCallId::new("terminal-1");
4735        let tool_call = acp::ToolCall::new(tool_call_id.clone(), "Run `cargo build --release`")
4736            .kind(acp::ToolKind::Edit);
4737
4738        let permission_options = ToolPermissionContext::new(
4739            TerminalTool::NAME,
4740            vec!["cargo build --release".to_string()],
4741        )
4742        .build_permission_options();
4743
4744        let connection =
4745            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
4746                tool_call_id.clone(),
4747                permission_options,
4748            )]));
4749
4750        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
4751
4752        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
4753
4754        // Disable notifications to avoid popup windows
4755        cx.update(|_window, cx| {
4756            AgentSettings::override_global(
4757                AgentSettings {
4758                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
4759                    ..AgentSettings::get_global(cx).clone()
4760                },
4761                cx,
4762            );
4763        });
4764
4765        let message_editor = message_editor(&thread_view, cx);
4766        message_editor.update_in(cx, |editor, window, cx| {
4767            editor.set_text("Run cargo build", window, cx);
4768        });
4769
4770        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
4771
4772        cx.run_until_parked();
4773
4774        // Verify the tool call is in WaitingForConfirmation state with the expected options
4775        thread_view.read_with(cx, |thread_view, cx| {
4776            let thread = thread_view
4777                .active_thread()
4778                .expect("Thread should exist")
4779                .read(cx)
4780                .thread
4781                .clone();
4782            let thread = thread.read(cx);
4783
4784            let tool_call = thread.entries().iter().find_map(|entry| {
4785                if let acp_thread::AgentThreadEntry::ToolCall(call) = entry {
4786                    Some(call)
4787                } else {
4788                    None
4789                }
4790            });
4791
4792            assert!(tool_call.is_some(), "Expected a tool call entry");
4793            let tool_call = tool_call.unwrap();
4794
4795            // Verify it's waiting for confirmation
4796            assert!(
4797                matches!(
4798                    tool_call.status,
4799                    acp_thread::ToolCallStatus::WaitingForConfirmation { .. }
4800                ),
4801                "Expected WaitingForConfirmation status, got {:?}",
4802                tool_call.status
4803            );
4804
4805            // Verify the options count (granularity options only, no separate Deny option)
4806            if let acp_thread::ToolCallStatus::WaitingForConfirmation { options, .. } =
4807                &tool_call.status
4808            {
4809                let PermissionOptions::Dropdown(choices) = options else {
4810                    panic!("Expected dropdown permission options");
4811                };
4812
4813                assert_eq!(
4814                    choices.len(),
4815                    3,
4816                    "Expected 3 permission options (granularity only)"
4817                );
4818
4819                // Verify specific button labels (now using neutral names)
4820                let labels: Vec<&str> = choices
4821                    .iter()
4822                    .map(|choice| choice.allow.name.as_ref())
4823                    .collect();
4824                assert!(
4825                    labels.contains(&"Always for terminal"),
4826                    "Missing 'Always for terminal' option"
4827                );
4828                assert!(
4829                    labels.contains(&"Always for `cargo` commands"),
4830                    "Missing pattern option"
4831                );
4832                assert!(
4833                    labels.contains(&"Only this time"),
4834                    "Missing 'Only this time' option"
4835                );
4836            }
4837        });
4838    }
4839
4840    #[gpui::test]
4841    async fn test_tool_permission_buttons_edit_file_with_path_pattern(cx: &mut TestAppContext) {
4842        init_test(cx);
4843
4844        let tool_call_id = acp::ToolCallId::new("edit-file-1");
4845        let tool_call = acp::ToolCall::new(tool_call_id.clone(), "Edit `src/main.rs`")
4846            .kind(acp::ToolKind::Edit);
4847
4848        let permission_options =
4849            ToolPermissionContext::new(EditFileTool::NAME, vec!["src/main.rs".to_string()])
4850                .build_permission_options();
4851
4852        let connection =
4853            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
4854                tool_call_id.clone(),
4855                permission_options,
4856            )]));
4857
4858        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
4859
4860        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
4861
4862        // Disable notifications
4863        cx.update(|_window, cx| {
4864            AgentSettings::override_global(
4865                AgentSettings {
4866                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
4867                    ..AgentSettings::get_global(cx).clone()
4868                },
4869                cx,
4870            );
4871        });
4872
4873        let message_editor = message_editor(&thread_view, cx);
4874        message_editor.update_in(cx, |editor, window, cx| {
4875            editor.set_text("Edit the main file", window, cx);
4876        });
4877
4878        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
4879
4880        cx.run_until_parked();
4881
4882        // Verify the options
4883        thread_view.read_with(cx, |thread_view, cx| {
4884            let thread = thread_view
4885                .active_thread()
4886                .expect("Thread should exist")
4887                .read(cx)
4888                .thread
4889                .clone();
4890            let thread = thread.read(cx);
4891
4892            let tool_call = thread.entries().iter().find_map(|entry| {
4893                if let acp_thread::AgentThreadEntry::ToolCall(call) = entry {
4894                    Some(call)
4895                } else {
4896                    None
4897                }
4898            });
4899
4900            assert!(tool_call.is_some(), "Expected a tool call entry");
4901            let tool_call = tool_call.unwrap();
4902
4903            if let acp_thread::ToolCallStatus::WaitingForConfirmation { options, .. } =
4904                &tool_call.status
4905            {
4906                let PermissionOptions::Dropdown(choices) = options else {
4907                    panic!("Expected dropdown permission options");
4908                };
4909
4910                let labels: Vec<&str> = choices
4911                    .iter()
4912                    .map(|choice| choice.allow.name.as_ref())
4913                    .collect();
4914                assert!(
4915                    labels.contains(&"Always for edit file"),
4916                    "Missing 'Always for edit file' option"
4917                );
4918                assert!(
4919                    labels.contains(&"Always for `src/`"),
4920                    "Missing path pattern option"
4921                );
4922            } else {
4923                panic!("Expected WaitingForConfirmation status");
4924            }
4925        });
4926    }
4927
4928    #[gpui::test]
4929    async fn test_tool_permission_buttons_fetch_with_domain_pattern(cx: &mut TestAppContext) {
4930        init_test(cx);
4931
4932        let tool_call_id = acp::ToolCallId::new("fetch-1");
4933        let tool_call = acp::ToolCall::new(tool_call_id.clone(), "Fetch `https://docs.rs/gpui`")
4934            .kind(acp::ToolKind::Fetch);
4935
4936        let permission_options =
4937            ToolPermissionContext::new(FetchTool::NAME, vec!["https://docs.rs/gpui".to_string()])
4938                .build_permission_options();
4939
4940        let connection =
4941            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
4942                tool_call_id.clone(),
4943                permission_options,
4944            )]));
4945
4946        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
4947
4948        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
4949
4950        // Disable notifications
4951        cx.update(|_window, cx| {
4952            AgentSettings::override_global(
4953                AgentSettings {
4954                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
4955                    ..AgentSettings::get_global(cx).clone()
4956                },
4957                cx,
4958            );
4959        });
4960
4961        let message_editor = message_editor(&thread_view, cx);
4962        message_editor.update_in(cx, |editor, window, cx| {
4963            editor.set_text("Fetch the docs", window, cx);
4964        });
4965
4966        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
4967
4968        cx.run_until_parked();
4969
4970        // Verify the options
4971        thread_view.read_with(cx, |thread_view, cx| {
4972            let thread = thread_view
4973                .active_thread()
4974                .expect("Thread should exist")
4975                .read(cx)
4976                .thread
4977                .clone();
4978            let thread = thread.read(cx);
4979
4980            let tool_call = thread.entries().iter().find_map(|entry| {
4981                if let acp_thread::AgentThreadEntry::ToolCall(call) = entry {
4982                    Some(call)
4983                } else {
4984                    None
4985                }
4986            });
4987
4988            assert!(tool_call.is_some(), "Expected a tool call entry");
4989            let tool_call = tool_call.unwrap();
4990
4991            if let acp_thread::ToolCallStatus::WaitingForConfirmation { options, .. } =
4992                &tool_call.status
4993            {
4994                let PermissionOptions::Dropdown(choices) = options else {
4995                    panic!("Expected dropdown permission options");
4996                };
4997
4998                let labels: Vec<&str> = choices
4999                    .iter()
5000                    .map(|choice| choice.allow.name.as_ref())
5001                    .collect();
5002                assert!(
5003                    labels.contains(&"Always for fetch"),
5004                    "Missing 'Always for fetch' option"
5005                );
5006                assert!(
5007                    labels.contains(&"Always for `docs.rs`"),
5008                    "Missing domain pattern option"
5009                );
5010            } else {
5011                panic!("Expected WaitingForConfirmation status");
5012            }
5013        });
5014    }
5015
5016    #[gpui::test]
5017    async fn test_tool_permission_buttons_without_pattern(cx: &mut TestAppContext) {
5018        init_test(cx);
5019
5020        let tool_call_id = acp::ToolCallId::new("terminal-no-pattern-1");
5021        let tool_call = acp::ToolCall::new(tool_call_id.clone(), "Run `./deploy.sh --production`")
5022            .kind(acp::ToolKind::Edit);
5023
5024        // No pattern button since ./deploy.sh doesn't match the alphanumeric pattern
5025        let permission_options = ToolPermissionContext::new(
5026            TerminalTool::NAME,
5027            vec!["./deploy.sh --production".to_string()],
5028        )
5029        .build_permission_options();
5030
5031        let connection =
5032            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
5033                tool_call_id.clone(),
5034                permission_options,
5035            )]));
5036
5037        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
5038
5039        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
5040
5041        // Disable notifications
5042        cx.update(|_window, cx| {
5043            AgentSettings::override_global(
5044                AgentSettings {
5045                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
5046                    ..AgentSettings::get_global(cx).clone()
5047                },
5048                cx,
5049            );
5050        });
5051
5052        let message_editor = message_editor(&thread_view, cx);
5053        message_editor.update_in(cx, |editor, window, cx| {
5054            editor.set_text("Run the deploy script", window, cx);
5055        });
5056
5057        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
5058
5059        cx.run_until_parked();
5060
5061        // Verify only 2 options (no pattern button when command doesn't match pattern)
5062        thread_view.read_with(cx, |thread_view, cx| {
5063            let thread = thread_view
5064                .active_thread()
5065                .expect("Thread should exist")
5066                .read(cx)
5067                .thread
5068                .clone();
5069            let thread = thread.read(cx);
5070
5071            let tool_call = thread.entries().iter().find_map(|entry| {
5072                if let acp_thread::AgentThreadEntry::ToolCall(call) = entry {
5073                    Some(call)
5074                } else {
5075                    None
5076                }
5077            });
5078
5079            assert!(tool_call.is_some(), "Expected a tool call entry");
5080            let tool_call = tool_call.unwrap();
5081
5082            if let acp_thread::ToolCallStatus::WaitingForConfirmation { options, .. } =
5083                &tool_call.status
5084            {
5085                let PermissionOptions::Dropdown(choices) = options else {
5086                    panic!("Expected dropdown permission options");
5087                };
5088
5089                assert_eq!(
5090                    choices.len(),
5091                    2,
5092                    "Expected 2 permission options (no pattern option)"
5093                );
5094
5095                let labels: Vec<&str> = choices
5096                    .iter()
5097                    .map(|choice| choice.allow.name.as_ref())
5098                    .collect();
5099                assert!(
5100                    labels.contains(&"Always for terminal"),
5101                    "Missing 'Always for terminal' option"
5102                );
5103                assert!(
5104                    labels.contains(&"Only this time"),
5105                    "Missing 'Only this time' option"
5106                );
5107                // Should NOT contain a pattern option
5108                assert!(
5109                    !labels.iter().any(|l| l.contains("commands")),
5110                    "Should not have pattern option"
5111                );
5112            } else {
5113                panic!("Expected WaitingForConfirmation status");
5114            }
5115        });
5116    }
5117
5118    #[gpui::test]
5119    async fn test_authorize_tool_call_action_triggers_authorization(cx: &mut TestAppContext) {
5120        init_test(cx);
5121
5122        let tool_call_id = acp::ToolCallId::new("action-test-1");
5123        let tool_call =
5124            acp::ToolCall::new(tool_call_id.clone(), "Run `cargo test`").kind(acp::ToolKind::Edit);
5125
5126        let permission_options =
5127            ToolPermissionContext::new(TerminalTool::NAME, vec!["cargo test".to_string()])
5128                .build_permission_options();
5129
5130        let connection =
5131            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
5132                tool_call_id.clone(),
5133                permission_options,
5134            )]));
5135
5136        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
5137
5138        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
5139        add_to_workspace(thread_view.clone(), cx);
5140
5141        cx.update(|_window, cx| {
5142            AgentSettings::override_global(
5143                AgentSettings {
5144                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
5145                    ..AgentSettings::get_global(cx).clone()
5146                },
5147                cx,
5148            );
5149        });
5150
5151        let message_editor = message_editor(&thread_view, cx);
5152        message_editor.update_in(cx, |editor, window, cx| {
5153            editor.set_text("Run tests", window, cx);
5154        });
5155
5156        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
5157
5158        cx.run_until_parked();
5159
5160        // Verify tool call is waiting for confirmation
5161        thread_view.read_with(cx, |thread_view, cx| {
5162            let thread = thread_view
5163                .active_thread()
5164                .expect("Thread should exist")
5165                .read(cx)
5166                .thread
5167                .clone();
5168            let thread = thread.read(cx);
5169            let tool_call = thread.first_tool_awaiting_confirmation();
5170            assert!(
5171                tool_call.is_some(),
5172                "Expected a tool call waiting for confirmation"
5173            );
5174        });
5175
5176        // Dispatch the AuthorizeToolCall action (simulating dropdown menu selection)
5177        thread_view.update_in(cx, |_, window, cx| {
5178            window.dispatch_action(
5179                crate::AuthorizeToolCall {
5180                    tool_call_id: "action-test-1".to_string(),
5181                    option_id: "allow".to_string(),
5182                    option_kind: "AllowOnce".to_string(),
5183                }
5184                .boxed_clone(),
5185                cx,
5186            );
5187        });
5188
5189        cx.run_until_parked();
5190
5191        // Verify tool call is no longer waiting for confirmation (was authorized)
5192        thread_view.read_with(cx, |thread_view, cx| {
5193                let thread = thread_view.active_thread().expect("Thread should exist").read(cx).thread.clone();
5194                let thread = thread.read(cx);
5195                let tool_call = thread.first_tool_awaiting_confirmation();
5196                assert!(
5197                    tool_call.is_none(),
5198                    "Tool call should no longer be waiting for confirmation after AuthorizeToolCall action"
5199                );
5200            });
5201    }
5202
5203    #[gpui::test]
5204    async fn test_authorize_tool_call_action_with_pattern_option(cx: &mut TestAppContext) {
5205        init_test(cx);
5206
5207        let tool_call_id = acp::ToolCallId::new("pattern-action-test-1");
5208        let tool_call =
5209            acp::ToolCall::new(tool_call_id.clone(), "Run `npm install`").kind(acp::ToolKind::Edit);
5210
5211        let permission_options =
5212            ToolPermissionContext::new(TerminalTool::NAME, vec!["npm install".to_string()])
5213                .build_permission_options();
5214
5215        let connection =
5216            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
5217                tool_call_id.clone(),
5218                permission_options.clone(),
5219            )]));
5220
5221        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
5222
5223        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
5224        add_to_workspace(thread_view.clone(), cx);
5225
5226        cx.update(|_window, cx| {
5227            AgentSettings::override_global(
5228                AgentSettings {
5229                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
5230                    ..AgentSettings::get_global(cx).clone()
5231                },
5232                cx,
5233            );
5234        });
5235
5236        let message_editor = message_editor(&thread_view, cx);
5237        message_editor.update_in(cx, |editor, window, cx| {
5238            editor.set_text("Install dependencies", window, cx);
5239        });
5240
5241        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
5242
5243        cx.run_until_parked();
5244
5245        // Find the pattern option ID
5246        let pattern_option = match &permission_options {
5247            PermissionOptions::Dropdown(choices) => choices
5248                .iter()
5249                .find(|choice| {
5250                    choice
5251                        .allow
5252                        .option_id
5253                        .0
5254                        .starts_with("always_allow_pattern:")
5255                })
5256                .map(|choice| &choice.allow)
5257                .expect("Should have a pattern option for npm command"),
5258            _ => panic!("Expected dropdown permission options"),
5259        };
5260
5261        // Dispatch action with the pattern option (simulating "Always allow `npm` commands")
5262        thread_view.update_in(cx, |_, window, cx| {
5263            window.dispatch_action(
5264                crate::AuthorizeToolCall {
5265                    tool_call_id: "pattern-action-test-1".to_string(),
5266                    option_id: pattern_option.option_id.0.to_string(),
5267                    option_kind: "AllowAlways".to_string(),
5268                }
5269                .boxed_clone(),
5270                cx,
5271            );
5272        });
5273
5274        cx.run_until_parked();
5275
5276        // Verify tool call was authorized
5277        thread_view.read_with(cx, |thread_view, cx| {
5278            let thread = thread_view
5279                .active_thread()
5280                .expect("Thread should exist")
5281                .read(cx)
5282                .thread
5283                .clone();
5284            let thread = thread.read(cx);
5285            let tool_call = thread.first_tool_awaiting_confirmation();
5286            assert!(
5287                tool_call.is_none(),
5288                "Tool call should be authorized after selecting pattern option"
5289            );
5290        });
5291    }
5292
5293    #[gpui::test]
5294    async fn test_granularity_selection_updates_state(cx: &mut TestAppContext) {
5295        init_test(cx);
5296
5297        let tool_call_id = acp::ToolCallId::new("granularity-test-1");
5298        let tool_call =
5299            acp::ToolCall::new(tool_call_id.clone(), "Run `cargo build`").kind(acp::ToolKind::Edit);
5300
5301        let permission_options =
5302            ToolPermissionContext::new(TerminalTool::NAME, vec!["cargo build".to_string()])
5303                .build_permission_options();
5304
5305        let connection =
5306            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
5307                tool_call_id.clone(),
5308                permission_options.clone(),
5309            )]));
5310
5311        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
5312
5313        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
5314        add_to_workspace(thread_view.clone(), cx);
5315
5316        cx.update(|_window, cx| {
5317            AgentSettings::override_global(
5318                AgentSettings {
5319                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
5320                    ..AgentSettings::get_global(cx).clone()
5321                },
5322                cx,
5323            );
5324        });
5325
5326        let message_editor = message_editor(&thread_view, cx);
5327        message_editor.update_in(cx, |editor, window, cx| {
5328            editor.set_text("Build the project", window, cx);
5329        });
5330
5331        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
5332
5333        cx.run_until_parked();
5334
5335        // Verify default granularity is the last option (index 2 = "Only this time")
5336        thread_view.read_with(cx, |thread_view, cx| {
5337            let state = thread_view.active_thread().unwrap();
5338            let selected = state
5339                .read(cx)
5340                .selected_permission_granularity
5341                .get(&tool_call_id);
5342            assert!(
5343                selected.is_none(),
5344                "Should have no selection initially (defaults to last)"
5345            );
5346        });
5347
5348        // Select the first option (index 0 = "Always for terminal")
5349        thread_view.update_in(cx, |_, window, cx| {
5350            window.dispatch_action(
5351                crate::SelectPermissionGranularity {
5352                    tool_call_id: "granularity-test-1".to_string(),
5353                    index: 0,
5354                }
5355                .boxed_clone(),
5356                cx,
5357            );
5358        });
5359
5360        cx.run_until_parked();
5361
5362        // Verify the selection was updated
5363        thread_view.read_with(cx, |thread_view, cx| {
5364            let state = thread_view.active_thread().unwrap();
5365            let selected = state
5366                .read(cx)
5367                .selected_permission_granularity
5368                .get(&tool_call_id);
5369            assert_eq!(selected, Some(&0), "Should have selected index 0");
5370        });
5371    }
5372
5373    #[gpui::test]
5374    async fn test_allow_button_uses_selected_granularity(cx: &mut TestAppContext) {
5375        init_test(cx);
5376
5377        let tool_call_id = acp::ToolCallId::new("allow-granularity-test-1");
5378        let tool_call =
5379            acp::ToolCall::new(tool_call_id.clone(), "Run `npm install`").kind(acp::ToolKind::Edit);
5380
5381        let permission_options =
5382            ToolPermissionContext::new(TerminalTool::NAME, vec!["npm install".to_string()])
5383                .build_permission_options();
5384
5385        // Verify we have the expected options
5386        let PermissionOptions::Dropdown(choices) = &permission_options else {
5387            panic!("Expected dropdown permission options");
5388        };
5389
5390        assert_eq!(choices.len(), 3);
5391        assert!(
5392            choices[0]
5393                .allow
5394                .option_id
5395                .0
5396                .contains("always_allow:terminal")
5397        );
5398        assert!(
5399            choices[1]
5400                .allow
5401                .option_id
5402                .0
5403                .contains("always_allow_pattern:terminal")
5404        );
5405        assert_eq!(choices[2].allow.option_id.0.as_ref(), "allow");
5406
5407        let connection =
5408            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
5409                tool_call_id.clone(),
5410                permission_options.clone(),
5411            )]));
5412
5413        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
5414
5415        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
5416        add_to_workspace(thread_view.clone(), cx);
5417
5418        cx.update(|_window, cx| {
5419            AgentSettings::override_global(
5420                AgentSettings {
5421                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
5422                    ..AgentSettings::get_global(cx).clone()
5423                },
5424                cx,
5425            );
5426        });
5427
5428        let message_editor = message_editor(&thread_view, cx);
5429        message_editor.update_in(cx, |editor, window, cx| {
5430            editor.set_text("Install dependencies", window, cx);
5431        });
5432
5433        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
5434
5435        cx.run_until_parked();
5436
5437        // Select the pattern option (index 1 = "Always for `npm` commands")
5438        thread_view.update_in(cx, |_, window, cx| {
5439            window.dispatch_action(
5440                crate::SelectPermissionGranularity {
5441                    tool_call_id: "allow-granularity-test-1".to_string(),
5442                    index: 1,
5443                }
5444                .boxed_clone(),
5445                cx,
5446            );
5447        });
5448
5449        cx.run_until_parked();
5450
5451        // Simulate clicking the Allow button by dispatching AllowOnce action
5452        // which should use the selected granularity
5453        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| {
5454            view.allow_once(&AllowOnce, window, cx)
5455        });
5456
5457        cx.run_until_parked();
5458
5459        // Verify tool call was authorized
5460        thread_view.read_with(cx, |thread_view, cx| {
5461            let thread = thread_view
5462                .active_thread()
5463                .expect("Thread should exist")
5464                .read(cx)
5465                .thread
5466                .clone();
5467            let thread = thread.read(cx);
5468            let tool_call = thread.first_tool_awaiting_confirmation();
5469            assert!(
5470                tool_call.is_none(),
5471                "Tool call should be authorized after Allow with pattern granularity"
5472            );
5473        });
5474    }
5475
5476    #[gpui::test]
5477    async fn test_deny_button_uses_selected_granularity(cx: &mut TestAppContext) {
5478        init_test(cx);
5479
5480        let tool_call_id = acp::ToolCallId::new("deny-granularity-test-1");
5481        let tool_call =
5482            acp::ToolCall::new(tool_call_id.clone(), "Run `git push`").kind(acp::ToolKind::Edit);
5483
5484        let permission_options =
5485            ToolPermissionContext::new(TerminalTool::NAME, vec!["git push".to_string()])
5486                .build_permission_options();
5487
5488        let connection =
5489            StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
5490                tool_call_id.clone(),
5491                permission_options.clone(),
5492            )]));
5493
5494        connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
5495
5496        let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
5497        add_to_workspace(thread_view.clone(), cx);
5498
5499        cx.update(|_window, cx| {
5500            AgentSettings::override_global(
5501                AgentSettings {
5502                    notify_when_agent_waiting: NotifyWhenAgentWaiting::Never,
5503                    ..AgentSettings::get_global(cx).clone()
5504                },
5505                cx,
5506            );
5507        });
5508
5509        let message_editor = message_editor(&thread_view, cx);
5510        message_editor.update_in(cx, |editor, window, cx| {
5511            editor.set_text("Push changes", window, cx);
5512        });
5513
5514        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| view.send(window, cx));
5515
5516        cx.run_until_parked();
5517
5518        // Use default granularity (last option = "Only this time")
5519        // Simulate clicking the Deny button
5520        active_thread(&thread_view, cx).update_in(cx, |view, window, cx| {
5521            view.reject_once(&RejectOnce, window, cx)
5522        });
5523
5524        cx.run_until_parked();
5525
5526        // Verify tool call was rejected (no longer waiting for confirmation)
5527        thread_view.read_with(cx, |thread_view, cx| {
5528            let thread = thread_view
5529                .active_thread()
5530                .expect("Thread should exist")
5531                .read(cx)
5532                .thread
5533                .clone();
5534            let thread = thread.read(cx);
5535            let tool_call = thread.first_tool_awaiting_confirmation();
5536            assert!(
5537                tool_call.is_none(),
5538                "Tool call should be rejected after Deny"
5539            );
5540        });
5541    }
5542
5543    #[gpui::test]
5544    async fn test_option_id_transformation_for_allow() {
5545        let permission_options = ToolPermissionContext::new(
5546            TerminalTool::NAME,
5547            vec!["cargo build --release".to_string()],
5548        )
5549        .build_permission_options();
5550
5551        let PermissionOptions::Dropdown(choices) = permission_options else {
5552            panic!("Expected dropdown permission options");
5553        };
5554
5555        let allow_ids: Vec<String> = choices
5556            .iter()
5557            .map(|choice| choice.allow.option_id.0.to_string())
5558            .collect();
5559
5560        assert!(allow_ids.contains(&"always_allow:terminal".to_string()));
5561        assert!(allow_ids.contains(&"allow".to_string()));
5562        assert!(
5563            allow_ids
5564                .iter()
5565                .any(|id| id.starts_with("always_allow_pattern:terminal\n")),
5566            "Missing allow pattern option"
5567        );
5568    }
5569
5570    #[gpui::test]
5571    async fn test_option_id_transformation_for_deny() {
5572        let permission_options = ToolPermissionContext::new(
5573            TerminalTool::NAME,
5574            vec!["cargo build --release".to_string()],
5575        )
5576        .build_permission_options();
5577
5578        let PermissionOptions::Dropdown(choices) = permission_options else {
5579            panic!("Expected dropdown permission options");
5580        };
5581
5582        let deny_ids: Vec<String> = choices
5583            .iter()
5584            .map(|choice| choice.deny.option_id.0.to_string())
5585            .collect();
5586
5587        assert!(deny_ids.contains(&"always_deny:terminal".to_string()));
5588        assert!(deny_ids.contains(&"deny".to_string()));
5589        assert!(
5590            deny_ids
5591                .iter()
5592                .any(|id| id.starts_with("always_deny_pattern:terminal\n")),
5593            "Missing deny pattern option"
5594        );
5595    }
5596}