conversation_view.rs

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