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