connection.rs

   1use crate::AcpThread;
   2use agent_client_protocol::{self as acp};
   3use anyhow::Result;
   4use chrono::{DateTime, Utc};
   5use collections::{HashMap, IndexMap};
   6use gpui::{Entity, SharedString, Task};
   7use language_model::LanguageModelProviderId;
   8use project::{AgentId, Project};
   9use serde::{Deserialize, Serialize};
  10use std::{any::Any, error::Error, fmt, path::PathBuf, rc::Rc, sync::Arc};
  11use task::{HideStrategy, SpawnInTerminal, TaskId};
  12use ui::{App, IconName};
  13use util::path_list::PathList;
  14use uuid::Uuid;
  15
  16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
  17pub struct UserMessageId(Arc<str>);
  18
  19impl UserMessageId {
  20    pub fn new() -> Self {
  21        Self(Uuid::new_v4().to_string().into())
  22    }
  23}
  24
  25pub fn build_terminal_auth_task(
  26    id: String,
  27    label: String,
  28    command: String,
  29    args: Vec<String>,
  30    env: HashMap<String, String>,
  31) -> SpawnInTerminal {
  32    SpawnInTerminal {
  33        id: TaskId(id),
  34        full_label: label.clone(),
  35        label: label.clone(),
  36        command: Some(command),
  37        args,
  38        command_label: label,
  39        env,
  40        use_new_terminal: true,
  41        allow_concurrent_runs: true,
  42        hide: HideStrategy::Always,
  43        ..Default::default()
  44    }
  45}
  46
  47pub trait AgentConnection {
  48    fn agent_id(&self) -> AgentId;
  49
  50    fn telemetry_id(&self) -> SharedString;
  51
  52    fn new_session(
  53        self: Rc<Self>,
  54        project: Entity<Project>,
  55        _work_dirs: PathList,
  56        cx: &mut App,
  57    ) -> Task<Result<Entity<AcpThread>>>;
  58
  59    /// Whether this agent supports loading existing sessions.
  60    fn supports_load_session(&self) -> bool {
  61        false
  62    }
  63
  64    /// Load an existing session by ID.
  65    fn load_session(
  66        self: Rc<Self>,
  67        _session_id: acp::SessionId,
  68        _project: Entity<Project>,
  69        _work_dirs: PathList,
  70        _title: Option<SharedString>,
  71        _cx: &mut App,
  72    ) -> Task<Result<Entity<AcpThread>>> {
  73        Task::ready(Err(anyhow::Error::msg("Loading sessions is not supported")))
  74    }
  75
  76    /// Whether this agent supports closing existing sessions.
  77    fn supports_close_session(&self) -> bool {
  78        false
  79    }
  80
  81    /// Close an existing session. Allows the agent to free the session from memory.
  82    fn close_session(
  83        self: Rc<Self>,
  84        _session_id: &acp::SessionId,
  85        _cx: &mut App,
  86    ) -> Task<Result<()>> {
  87        Task::ready(Err(anyhow::Error::msg("Closing sessions is not supported")))
  88    }
  89
  90    /// Whether this agent supports resuming existing sessions without loading history.
  91    fn supports_resume_session(&self) -> bool {
  92        false
  93    }
  94
  95    /// Resume an existing session by ID without replaying previous messages.
  96    fn resume_session(
  97        self: Rc<Self>,
  98        _session_id: acp::SessionId,
  99        _project: Entity<Project>,
 100        _work_dirs: PathList,
 101        _title: Option<SharedString>,
 102        _cx: &mut App,
 103    ) -> Task<Result<Entity<AcpThread>>> {
 104        Task::ready(Err(anyhow::Error::msg(
 105            "Resuming sessions is not supported",
 106        )))
 107    }
 108
 109    /// Whether this agent supports showing session history.
 110    fn supports_session_history(&self) -> bool {
 111        self.supports_load_session() || self.supports_resume_session()
 112    }
 113
 114    fn auth_methods(&self) -> &[acp::AuthMethod];
 115
 116    fn terminal_auth_task(
 117        &self,
 118        _method: &acp::AuthMethodId,
 119        _cx: &App,
 120    ) -> Option<Task<Result<SpawnInTerminal>>> {
 121        None
 122    }
 123
 124    fn authenticate(&self, method: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>>;
 125
 126    fn prompt(
 127        &self,
 128        user_message_id: UserMessageId,
 129        params: acp::PromptRequest,
 130        cx: &mut App,
 131    ) -> Task<Result<acp::PromptResponse>>;
 132
 133    fn retry(&self, _session_id: &acp::SessionId, _cx: &App) -> Option<Rc<dyn AgentSessionRetry>> {
 134        None
 135    }
 136
 137    fn cancel(&self, session_id: &acp::SessionId, cx: &mut App);
 138
 139    fn truncate(
 140        &self,
 141        _session_id: &acp::SessionId,
 142        _cx: &App,
 143    ) -> Option<Rc<dyn AgentSessionTruncate>> {
 144        None
 145    }
 146
 147    fn set_title(
 148        &self,
 149        _session_id: &acp::SessionId,
 150        _cx: &App,
 151    ) -> Option<Rc<dyn AgentSessionSetTitle>> {
 152        None
 153    }
 154
 155    /// Returns this agent as an [Rc<dyn ModelSelector>] if the model selection capability is supported.
 156    ///
 157    /// If the agent does not support model selection, returns [None].
 158    /// This allows sharing the selector in UI components.
 159    fn model_selector(&self, _session_id: &acp::SessionId) -> Option<Rc<dyn AgentModelSelector>> {
 160        None
 161    }
 162
 163    fn telemetry(&self) -> Option<Rc<dyn AgentTelemetry>> {
 164        None
 165    }
 166
 167    fn session_modes(
 168        &self,
 169        _session_id: &acp::SessionId,
 170        _cx: &App,
 171    ) -> Option<Rc<dyn AgentSessionModes>> {
 172        None
 173    }
 174
 175    fn session_config_options(
 176        &self,
 177        _session_id: &acp::SessionId,
 178        _cx: &App,
 179    ) -> Option<Rc<dyn AgentSessionConfigOptions>> {
 180        None
 181    }
 182
 183    fn session_list(&self, _cx: &mut App) -> Option<Rc<dyn AgentSessionList>> {
 184        None
 185    }
 186
 187    fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
 188}
 189
 190impl dyn AgentConnection {
 191    pub fn downcast<T: 'static + AgentConnection + Sized>(self: Rc<Self>) -> Option<Rc<T>> {
 192        self.into_any().downcast().ok()
 193    }
 194}
 195
 196pub trait AgentSessionTruncate {
 197    fn run(&self, message_id: UserMessageId, cx: &mut App) -> Task<Result<()>>;
 198}
 199
 200pub trait AgentSessionRetry {
 201    fn run(&self, cx: &mut App) -> Task<Result<acp::PromptResponse>>;
 202}
 203
 204pub trait AgentSessionSetTitle {
 205    fn run(&self, title: SharedString, cx: &mut App) -> Task<Result<()>>;
 206}
 207
 208pub trait AgentTelemetry {
 209    /// A representation of the current thread state that can be serialized for
 210    /// storage with telemetry events.
 211    fn thread_data(
 212        &self,
 213        session_id: &acp::SessionId,
 214        cx: &mut App,
 215    ) -> Task<Result<serde_json::Value>>;
 216}
 217
 218pub trait AgentSessionModes {
 219    fn current_mode(&self) -> acp::SessionModeId;
 220
 221    fn all_modes(&self) -> Vec<acp::SessionMode>;
 222
 223    fn set_mode(&self, mode: acp::SessionModeId, cx: &mut App) -> Task<Result<()>>;
 224}
 225
 226pub trait AgentSessionConfigOptions {
 227    /// Get all current config options with their state
 228    fn config_options(&self) -> Vec<acp::SessionConfigOption>;
 229
 230    /// Set a config option value
 231    /// Returns the full updated list of config options
 232    fn set_config_option(
 233        &self,
 234        config_id: acp::SessionConfigId,
 235        value: acp::SessionConfigValueId,
 236        cx: &mut App,
 237    ) -> Task<Result<Vec<acp::SessionConfigOption>>>;
 238
 239    /// Whenever the config options are updated the receiver will be notified.
 240    /// Optional for agents that don't update their config options dynamically.
 241    fn watch(&self, _cx: &mut App) -> Option<watch::Receiver<()>> {
 242        None
 243    }
 244}
 245
 246#[derive(Debug, Clone, Default)]
 247pub struct AgentSessionListRequest {
 248    pub cwd: Option<PathBuf>,
 249    pub cursor: Option<String>,
 250    pub meta: Option<acp::Meta>,
 251}
 252
 253#[derive(Debug, Clone)]
 254pub struct AgentSessionListResponse {
 255    pub sessions: Vec<AgentSessionInfo>,
 256    pub next_cursor: Option<String>,
 257    pub meta: Option<acp::Meta>,
 258}
 259
 260impl AgentSessionListResponse {
 261    pub fn new(sessions: Vec<AgentSessionInfo>) -> Self {
 262        Self {
 263            sessions,
 264            next_cursor: None,
 265            meta: None,
 266        }
 267    }
 268}
 269
 270#[derive(Debug, Clone, PartialEq)]
 271pub struct AgentSessionInfo {
 272    pub session_id: acp::SessionId,
 273    pub work_dirs: Option<PathList>,
 274    pub title: Option<SharedString>,
 275    pub updated_at: Option<DateTime<Utc>>,
 276    pub created_at: Option<DateTime<Utc>>,
 277    pub meta: Option<acp::Meta>,
 278}
 279
 280impl AgentSessionInfo {
 281    pub fn new(session_id: impl Into<acp::SessionId>) -> Self {
 282        Self {
 283            session_id: session_id.into(),
 284            work_dirs: None,
 285            title: None,
 286            updated_at: None,
 287            created_at: None,
 288            meta: None,
 289        }
 290    }
 291}
 292
 293#[derive(Debug, Clone)]
 294pub enum SessionListUpdate {
 295    Refresh,
 296    SessionInfo {
 297        session_id: acp::SessionId,
 298        update: acp::SessionInfoUpdate,
 299    },
 300}
 301
 302pub trait AgentSessionList {
 303    fn list_sessions(
 304        &self,
 305        request: AgentSessionListRequest,
 306        cx: &mut App,
 307    ) -> Task<Result<AgentSessionListResponse>>;
 308
 309    fn supports_delete(&self) -> bool {
 310        false
 311    }
 312
 313    fn delete_session(&self, _session_id: &acp::SessionId, _cx: &mut App) -> Task<Result<()>> {
 314        Task::ready(Err(anyhow::anyhow!("delete_session not supported")))
 315    }
 316
 317    fn delete_sessions(&self, _cx: &mut App) -> Task<Result<()>> {
 318        Task::ready(Err(anyhow::anyhow!("delete_sessions not supported")))
 319    }
 320
 321    fn watch(&self, _cx: &mut App) -> Option<smol::channel::Receiver<SessionListUpdate>> {
 322        None
 323    }
 324
 325    fn notify_refresh(&self) {}
 326
 327    fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
 328}
 329
 330impl dyn AgentSessionList {
 331    pub fn downcast<T: 'static + AgentSessionList + Sized>(self: Rc<Self>) -> Option<Rc<T>> {
 332        self.into_any().downcast().ok()
 333    }
 334}
 335
 336#[derive(Debug)]
 337pub struct AuthRequired {
 338    pub description: Option<String>,
 339    pub provider_id: Option<LanguageModelProviderId>,
 340}
 341
 342impl AuthRequired {
 343    pub fn new() -> Self {
 344        Self {
 345            description: None,
 346            provider_id: None,
 347        }
 348    }
 349
 350    pub fn with_description(mut self, description: String) -> Self {
 351        self.description = Some(description);
 352        self
 353    }
 354
 355    pub fn with_language_model_provider(mut self, provider_id: LanguageModelProviderId) -> Self {
 356        self.provider_id = Some(provider_id);
 357        self
 358    }
 359}
 360
 361impl Error for AuthRequired {}
 362impl fmt::Display for AuthRequired {
 363    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
 364        write!(f, "Authentication required")
 365    }
 366}
 367
 368/// Trait for agents that support listing, selecting, and querying language models.
 369///
 370/// This is an optional capability; agents indicate support via [AgentConnection::model_selector].
 371pub trait AgentModelSelector: 'static {
 372    /// Lists all available language models for this agent.
 373    ///
 374    /// # Parameters
 375    /// - `cx`: The GPUI app context for async operations and global access.
 376    ///
 377    /// # Returns
 378    /// A task resolving to the list of models or an error (e.g., if no models are configured).
 379    fn list_models(&self, cx: &mut App) -> Task<Result<AgentModelList>>;
 380
 381    /// Selects a model for a specific session (thread).
 382    ///
 383    /// This sets the default model for future interactions in the session.
 384    /// If the session doesn't exist or the model is invalid, it returns an error.
 385    ///
 386    /// # Parameters
 387    /// - `model`: The model to select (should be one from [list_models]).
 388    /// - `cx`: The GPUI app context.
 389    ///
 390    /// # Returns
 391    /// A task resolving to `Ok(())` on success or an error.
 392    fn select_model(&self, model_id: acp::ModelId, cx: &mut App) -> Task<Result<()>>;
 393
 394    /// Retrieves the currently selected model for a specific session (thread).
 395    ///
 396    /// # Parameters
 397    /// - `cx`: The GPUI app context.
 398    ///
 399    /// # Returns
 400    /// A task resolving to the selected model (always set) or an error (e.g., session not found).
 401    fn selected_model(&self, cx: &mut App) -> Task<Result<AgentModelInfo>>;
 402
 403    /// Whenever the model list is updated the receiver will be notified.
 404    /// Optional for agents that don't update their model list.
 405    fn watch(&self, _cx: &mut App) -> Option<watch::Receiver<()>> {
 406        None
 407    }
 408
 409    /// Returns whether the model picker should render a footer.
 410    fn should_render_footer(&self) -> bool {
 411        false
 412    }
 413}
 414
 415/// Icon for a model in the model selector.
 416#[derive(Debug, Clone, PartialEq, Eq)]
 417pub enum AgentModelIcon {
 418    /// A built-in icon from Zed's icon set.
 419    Named(IconName),
 420    /// Path to a custom SVG icon file.
 421    Path(SharedString),
 422}
 423
 424#[derive(Debug, Clone, PartialEq, Eq)]
 425pub struct AgentModelInfo {
 426    pub id: acp::ModelId,
 427    pub name: SharedString,
 428    pub description: Option<SharedString>,
 429    pub icon: Option<AgentModelIcon>,
 430    pub is_latest: bool,
 431    pub cost: Option<SharedString>,
 432}
 433
 434impl From<acp::ModelInfo> for AgentModelInfo {
 435    fn from(info: acp::ModelInfo) -> Self {
 436        Self {
 437            id: info.model_id,
 438            name: info.name.into(),
 439            description: info.description.map(|desc| desc.into()),
 440            icon: None,
 441            is_latest: false,
 442            cost: None,
 443        }
 444    }
 445}
 446
 447#[derive(Debug, Clone, PartialEq, Eq, Hash)]
 448pub struct AgentModelGroupName(pub SharedString);
 449
 450#[derive(Debug, Clone)]
 451pub enum AgentModelList {
 452    Flat(Vec<AgentModelInfo>),
 453    Grouped(IndexMap<AgentModelGroupName, Vec<AgentModelInfo>>),
 454}
 455
 456impl AgentModelList {
 457    pub fn is_empty(&self) -> bool {
 458        match self {
 459            AgentModelList::Flat(models) => models.is_empty(),
 460            AgentModelList::Grouped(groups) => groups.is_empty(),
 461        }
 462    }
 463
 464    pub fn is_flat(&self) -> bool {
 465        matches!(self, AgentModelList::Flat(_))
 466    }
 467}
 468
 469#[derive(Debug, Clone)]
 470pub struct PermissionOptionChoice {
 471    pub allow: acp::PermissionOption,
 472    pub deny: acp::PermissionOption,
 473    pub sub_patterns: Vec<String>,
 474}
 475
 476impl PermissionOptionChoice {
 477    pub fn label(&self) -> SharedString {
 478        self.allow.name.clone().into()
 479    }
 480
 481    /// Build a `SelectedPermissionOutcome` for this choice.
 482    ///
 483    /// If the choice carries `sub_patterns`, they are attached as
 484    /// `SelectedPermissionParams::Terminal`.
 485    pub fn build_outcome(&self, is_allow: bool) -> crate::SelectedPermissionOutcome {
 486        let option = if is_allow { &self.allow } else { &self.deny };
 487
 488        let params = if !self.sub_patterns.is_empty() {
 489            Some(crate::SelectedPermissionParams::Terminal {
 490                patterns: self.sub_patterns.clone(),
 491            })
 492        } else {
 493            None
 494        };
 495
 496        crate::SelectedPermissionOutcome::new(option.option_id.clone(), option.kind).params(params)
 497    }
 498}
 499
 500/// Pairs a tool's permission pattern with its display name
 501///
 502/// For example, a pattern of `^cargo\\s+build(\\s|$)` would display as `cargo
 503/// build`. It's handy to keep these together rather than trying to derive
 504/// one from the other.
 505#[derive(Debug, Clone, PartialEq)]
 506pub struct PermissionPattern {
 507    pub pattern: String,
 508    pub display_name: String,
 509}
 510
 511#[derive(Debug, Clone)]
 512pub enum PermissionOptions {
 513    Flat(Vec<acp::PermissionOption>),
 514    Dropdown(Vec<PermissionOptionChoice>),
 515    DropdownWithPatterns {
 516        choices: Vec<PermissionOptionChoice>,
 517        patterns: Vec<PermissionPattern>,
 518        tool_name: String,
 519    },
 520}
 521
 522impl PermissionOptions {
 523    pub fn is_empty(&self) -> bool {
 524        match self {
 525            PermissionOptions::Flat(options) => options.is_empty(),
 526            PermissionOptions::Dropdown(options) => options.is_empty(),
 527            PermissionOptions::DropdownWithPatterns { choices, .. } => choices.is_empty(),
 528        }
 529    }
 530
 531    pub fn first_option_of_kind(
 532        &self,
 533        kind: acp::PermissionOptionKind,
 534    ) -> Option<&acp::PermissionOption> {
 535        match self {
 536            PermissionOptions::Flat(options) => options.iter().find(|option| option.kind == kind),
 537            PermissionOptions::Dropdown(options) => options.iter().find_map(|choice| {
 538                if choice.allow.kind == kind {
 539                    Some(&choice.allow)
 540                } else if choice.deny.kind == kind {
 541                    Some(&choice.deny)
 542                } else {
 543                    None
 544                }
 545            }),
 546            PermissionOptions::DropdownWithPatterns { choices, .. } => {
 547                choices.iter().find_map(|choice| {
 548                    if choice.allow.kind == kind {
 549                        Some(&choice.allow)
 550                    } else if choice.deny.kind == kind {
 551                        Some(&choice.deny)
 552                    } else {
 553                        None
 554                    }
 555                })
 556            }
 557        }
 558    }
 559
 560    pub fn allow_once_option_id(&self) -> Option<acp::PermissionOptionId> {
 561        self.first_option_of_kind(acp::PermissionOptionKind::AllowOnce)
 562            .map(|option| option.option_id.clone())
 563    }
 564
 565    pub fn deny_once_option_id(&self) -> Option<acp::PermissionOptionId> {
 566        self.first_option_of_kind(acp::PermissionOptionKind::RejectOnce)
 567            .map(|option| option.option_id.clone())
 568    }
 569
 570    /// Build a `SelectedPermissionOutcome` for the `DropdownWithPatterns`
 571    /// variant when the user has checked specific pattern indices.
 572    ///
 573    /// Returns `Some` with the always-allow/deny outcome when at least one
 574    /// pattern is checked. Returns `None` when zero patterns are checked,
 575    /// signaling that the caller should degrade to allow-once / deny-once.
 576    ///
 577    /// Panics (debug) or returns `None` (release) if called on a non-
 578    /// `DropdownWithPatterns` variant.
 579    pub fn build_outcome_for_checked_patterns(
 580        &self,
 581        checked_indices: &[usize],
 582        is_allow: bool,
 583    ) -> Option<crate::SelectedPermissionOutcome> {
 584        let PermissionOptions::DropdownWithPatterns {
 585            choices, patterns, ..
 586        } = self
 587        else {
 588            debug_assert!(
 589                false,
 590                "build_outcome_for_checked_patterns called on non-DropdownWithPatterns"
 591            );
 592            return None;
 593        };
 594
 595        let checked_patterns: Vec<String> = patterns
 596            .iter()
 597            .enumerate()
 598            .filter(|(index, _)| checked_indices.contains(index))
 599            .map(|(_, cp)| cp.pattern.clone())
 600            .collect();
 601
 602        if checked_patterns.is_empty() {
 603            return None;
 604        }
 605
 606        // Use the first choice (the "Always" choice) as the base for the outcome.
 607        let always_choice = choices.first()?;
 608        let option = if is_allow {
 609            &always_choice.allow
 610        } else {
 611            &always_choice.deny
 612        };
 613
 614        let outcome = crate::SelectedPermissionOutcome::new(option.option_id.clone(), option.kind)
 615            .params(Some(crate::SelectedPermissionParams::Terminal {
 616                patterns: checked_patterns,
 617            }));
 618        Some(outcome)
 619    }
 620}
 621
 622#[cfg(feature = "test-support")]
 623mod test_support {
 624    //! Test-only stubs and helpers for acp_thread.
 625    //!
 626    //! This module is gated by the `test-support` feature and is not included
 627    //! in production builds. It provides:
 628    //! - `StubAgentConnection` for mocking agent connections in tests
 629    //! - `create_test_png_base64` for generating test images
 630
 631    use std::sync::Arc;
 632    use std::sync::atomic::{AtomicUsize, Ordering};
 633
 634    use action_log::ActionLog;
 635    use collections::HashMap;
 636    use futures::{channel::oneshot, future::try_join_all};
 637    use gpui::{AppContext as _, WeakEntity};
 638    use parking_lot::Mutex;
 639
 640    use super::*;
 641
 642    /// Creates a PNG image encoded as base64 for testing.
 643    ///
 644    /// Generates a solid-color PNG of the specified dimensions and returns
 645    /// it as a base64-encoded string suitable for use in `ImageContent`.
 646    pub fn create_test_png_base64(width: u32, height: u32, color: [u8; 4]) -> String {
 647        use image::ImageEncoder as _;
 648
 649        let mut png_data = Vec::new();
 650        {
 651            let encoder = image::codecs::png::PngEncoder::new(&mut png_data);
 652            let mut pixels = Vec::with_capacity((width * height * 4) as usize);
 653            for _ in 0..(width * height) {
 654                pixels.extend_from_slice(&color);
 655            }
 656            encoder
 657                .write_image(&pixels, width, height, image::ExtendedColorType::Rgba8)
 658                .expect("Failed to encode PNG");
 659        }
 660
 661        use image::EncodableLayout as _;
 662        base64::Engine::encode(
 663            &base64::engine::general_purpose::STANDARD,
 664            png_data.as_bytes(),
 665        )
 666    }
 667
 668    /// Test-scoped counter for generating unique session IDs across all
 669    /// `StubAgentConnection` instances within a test case. Set as a GPUI
 670    /// global in test init so each case starts fresh.
 671    pub struct StubSessionCounter(pub AtomicUsize);
 672    impl gpui::Global for StubSessionCounter {}
 673
 674    impl StubSessionCounter {
 675        pub fn next(cx: &App) -> usize {
 676            cx.try_global::<Self>()
 677                .map(|g| g.0.fetch_add(1, Ordering::SeqCst))
 678                .unwrap_or_else(|| {
 679                    static FALLBACK: AtomicUsize = AtomicUsize::new(0);
 680                    FALLBACK.fetch_add(1, Ordering::SeqCst)
 681                })
 682        }
 683    }
 684
 685    #[derive(Clone)]
 686    pub struct StubAgentConnection {
 687        sessions: Arc<Mutex<HashMap<acp::SessionId, Session>>>,
 688        permission_requests: HashMap<acp::ToolCallId, PermissionOptions>,
 689        next_prompt_updates: Arc<Mutex<Vec<acp::SessionUpdate>>>,
 690        supports_load_session: bool,
 691        agent_id: AgentId,
 692        telemetry_id: SharedString,
 693    }
 694
 695    struct Session {
 696        thread: WeakEntity<AcpThread>,
 697        response_tx: Option<oneshot::Sender<acp::StopReason>>,
 698    }
 699
 700    impl Default for StubAgentConnection {
 701        fn default() -> Self {
 702            Self::new()
 703        }
 704    }
 705
 706    impl StubAgentConnection {
 707        pub fn new() -> Self {
 708            Self {
 709                next_prompt_updates: Default::default(),
 710                permission_requests: HashMap::default(),
 711                sessions: Arc::default(),
 712                supports_load_session: false,
 713                agent_id: AgentId::new("stub"),
 714                telemetry_id: "stub".into(),
 715            }
 716        }
 717
 718        pub fn set_next_prompt_updates(&self, updates: Vec<acp::SessionUpdate>) {
 719            *self.next_prompt_updates.lock() = updates;
 720        }
 721
 722        pub fn with_permission_requests(
 723            mut self,
 724            permission_requests: HashMap<acp::ToolCallId, PermissionOptions>,
 725        ) -> Self {
 726            self.permission_requests = permission_requests;
 727            self
 728        }
 729
 730        pub fn with_supports_load_session(mut self, supports_load_session: bool) -> Self {
 731            self.supports_load_session = supports_load_session;
 732            self
 733        }
 734
 735        pub fn with_agent_id(mut self, agent_id: AgentId) -> Self {
 736            self.agent_id = agent_id;
 737            self
 738        }
 739
 740        pub fn with_telemetry_id(mut self, telemetry_id: SharedString) -> Self {
 741            self.telemetry_id = telemetry_id;
 742            self
 743        }
 744
 745        fn create_session(
 746            self: Rc<Self>,
 747            session_id: acp::SessionId,
 748            project: Entity<Project>,
 749            work_dirs: PathList,
 750            title: Option<SharedString>,
 751            cx: &mut gpui::App,
 752        ) -> Entity<AcpThread> {
 753            let action_log = cx.new(|_| ActionLog::new(project.clone()));
 754            let thread = cx.new(|cx| {
 755                AcpThread::new(
 756                    None,
 757                    title,
 758                    Some(work_dirs),
 759                    self.clone(),
 760                    project,
 761                    action_log,
 762                    session_id.clone(),
 763                    watch::Receiver::constant(
 764                        acp::PromptCapabilities::new()
 765                            .image(true)
 766                            .audio(true)
 767                            .embedded_context(true),
 768                    ),
 769                    cx,
 770                )
 771            });
 772            self.sessions.lock().insert(
 773                session_id,
 774                Session {
 775                    thread: thread.downgrade(),
 776                    response_tx: None,
 777                },
 778            );
 779            thread
 780        }
 781
 782        pub fn send_update(
 783            &self,
 784            session_id: acp::SessionId,
 785            update: acp::SessionUpdate,
 786            cx: &mut App,
 787        ) {
 788            assert!(
 789                self.next_prompt_updates.lock().is_empty(),
 790                "Use either send_update or set_next_prompt_updates"
 791            );
 792
 793            self.sessions
 794                .lock()
 795                .get(&session_id)
 796                .unwrap()
 797                .thread
 798                .update(cx, |thread, cx| {
 799                    thread.handle_session_update(update, cx).unwrap();
 800                })
 801                .unwrap();
 802        }
 803
 804        pub fn end_turn(&self, session_id: acp::SessionId, stop_reason: acp::StopReason) {
 805            self.sessions
 806                .lock()
 807                .get_mut(&session_id)
 808                .unwrap()
 809                .response_tx
 810                .take()
 811                .expect("No pending turn")
 812                .send(stop_reason)
 813                .unwrap();
 814        }
 815    }
 816
 817    impl AgentConnection for StubAgentConnection {
 818        fn agent_id(&self) -> AgentId {
 819            self.agent_id.clone()
 820        }
 821
 822        fn telemetry_id(&self) -> SharedString {
 823            self.telemetry_id.clone()
 824        }
 825
 826        fn auth_methods(&self) -> &[acp::AuthMethod] {
 827            &[]
 828        }
 829
 830        fn model_selector(
 831            &self,
 832            _session_id: &acp::SessionId,
 833        ) -> Option<Rc<dyn AgentModelSelector>> {
 834            Some(self.model_selector_impl())
 835        }
 836
 837        fn new_session(
 838            self: Rc<Self>,
 839            project: Entity<Project>,
 840            work_dirs: PathList,
 841            cx: &mut gpui::App,
 842        ) -> Task<gpui::Result<Entity<AcpThread>>> {
 843            let session_id = acp::SessionId::new(StubSessionCounter::next(cx).to_string());
 844            let thread = self.create_session(session_id, project, work_dirs, None, cx);
 845            Task::ready(Ok(thread))
 846        }
 847
 848        fn supports_load_session(&self) -> bool {
 849            self.supports_load_session
 850        }
 851
 852        fn load_session(
 853            self: Rc<Self>,
 854            session_id: acp::SessionId,
 855            project: Entity<Project>,
 856            work_dirs: PathList,
 857            title: Option<SharedString>,
 858            cx: &mut App,
 859        ) -> Task<Result<Entity<AcpThread>>> {
 860            if !self.supports_load_session {
 861                return Task::ready(Err(anyhow::Error::msg("Loading sessions is not supported")));
 862            }
 863
 864            let thread = self.create_session(session_id, project, work_dirs, title, cx);
 865            Task::ready(Ok(thread))
 866        }
 867
 868        fn authenticate(
 869            &self,
 870            _method_id: acp::AuthMethodId,
 871            _cx: &mut App,
 872        ) -> Task<gpui::Result<()>> {
 873            unimplemented!()
 874        }
 875
 876        fn prompt(
 877            &self,
 878            _id: UserMessageId,
 879            params: acp::PromptRequest,
 880            cx: &mut App,
 881        ) -> Task<gpui::Result<acp::PromptResponse>> {
 882            let mut sessions = self.sessions.lock();
 883            let Session {
 884                thread,
 885                response_tx,
 886            } = sessions.get_mut(&params.session_id).unwrap();
 887            let mut tasks = vec![];
 888            if self.next_prompt_updates.lock().is_empty() {
 889                let (tx, rx) = oneshot::channel();
 890                response_tx.replace(tx);
 891                cx.spawn(async move |_| {
 892                    let stop_reason = rx.await?;
 893                    Ok(acp::PromptResponse::new(stop_reason))
 894                })
 895            } else {
 896                for update in self.next_prompt_updates.lock().drain(..) {
 897                    let thread = thread.clone();
 898                    let update = update.clone();
 899                    let permission_request = if let acp::SessionUpdate::ToolCall(tool_call) =
 900                        &update
 901                        && let Some(options) = self.permission_requests.get(&tool_call.tool_call_id)
 902                    {
 903                        Some((tool_call.clone(), options.clone()))
 904                    } else {
 905                        None
 906                    };
 907                    let task = cx.spawn(async move |cx| {
 908                        if let Some((tool_call, options)) = permission_request {
 909                            thread
 910                                .update(cx, |thread, cx| {
 911                                    thread.request_tool_call_authorization(
 912                                        tool_call.clone().into(),
 913                                        options.clone(),
 914                                        cx,
 915                                    )
 916                                })??
 917                                .await;
 918                        }
 919                        thread.update(cx, |thread, cx| {
 920                            thread.handle_session_update(update.clone(), cx).unwrap();
 921                        })?;
 922                        anyhow::Ok(())
 923                    });
 924                    tasks.push(task);
 925                }
 926
 927                cx.spawn(async move |_| {
 928                    try_join_all(tasks).await?;
 929                    Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
 930                })
 931            }
 932        }
 933
 934        fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) {
 935            if let Some(end_turn_tx) = self
 936                .sessions
 937                .lock()
 938                .get_mut(session_id)
 939                .unwrap()
 940                .response_tx
 941                .take()
 942            {
 943                end_turn_tx.send(acp::StopReason::Cancelled).unwrap();
 944            }
 945        }
 946
 947        fn set_title(
 948            &self,
 949            _session_id: &acp::SessionId,
 950            _cx: &App,
 951        ) -> Option<Rc<dyn AgentSessionSetTitle>> {
 952            Some(Rc::new(StubAgentSessionSetTitle))
 953        }
 954
 955        fn truncate(
 956            &self,
 957            _session_id: &agent_client_protocol::SessionId,
 958            _cx: &App,
 959        ) -> Option<Rc<dyn AgentSessionTruncate>> {
 960            Some(Rc::new(StubAgentSessionEditor))
 961        }
 962
 963        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
 964            self
 965        }
 966    }
 967
 968    struct StubAgentSessionSetTitle;
 969
 970    impl AgentSessionSetTitle for StubAgentSessionSetTitle {
 971        fn run(&self, _title: SharedString, _cx: &mut App) -> Task<Result<()>> {
 972            Task::ready(Ok(()))
 973        }
 974    }
 975
 976    struct StubAgentSessionEditor;
 977
 978    impl AgentSessionTruncate for StubAgentSessionEditor {
 979        fn run(&self, _: UserMessageId, _: &mut App) -> Task<Result<()>> {
 980            Task::ready(Ok(()))
 981        }
 982    }
 983
 984    #[derive(Clone)]
 985    struct StubModelSelector {
 986        selected_model: Arc<Mutex<AgentModelInfo>>,
 987    }
 988
 989    impl StubModelSelector {
 990        fn new() -> Self {
 991            Self {
 992                selected_model: Arc::new(Mutex::new(AgentModelInfo {
 993                    id: acp::ModelId::new("visual-test-model"),
 994                    name: "Visual Test Model".into(),
 995                    description: Some("A stub model for visual testing".into()),
 996                    icon: Some(AgentModelIcon::Named(ui::IconName::ZedAssistant)),
 997                    is_latest: false,
 998                    cost: None,
 999                })),
1000            }
1001        }
1002    }
1003
1004    impl AgentModelSelector for StubModelSelector {
1005        fn list_models(&self, _cx: &mut App) -> Task<Result<AgentModelList>> {
1006            let model = self.selected_model.lock().clone();
1007            Task::ready(Ok(AgentModelList::Flat(vec![model])))
1008        }
1009
1010        fn select_model(&self, model_id: acp::ModelId, _cx: &mut App) -> Task<Result<()>> {
1011            self.selected_model.lock().id = model_id;
1012            Task::ready(Ok(()))
1013        }
1014
1015        fn selected_model(&self, _cx: &mut App) -> Task<Result<AgentModelInfo>> {
1016            Task::ready(Ok(self.selected_model.lock().clone()))
1017        }
1018    }
1019
1020    impl StubAgentConnection {
1021        /// Returns a model selector for this stub connection.
1022        pub fn model_selector_impl(&self) -> Rc<dyn AgentModelSelector> {
1023            Rc::new(StubModelSelector::new())
1024        }
1025    }
1026}
1027
1028#[cfg(feature = "test-support")]
1029pub use test_support::*;