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<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: Option<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    #[derive(Clone)]
 669    pub struct StubAgentConnection {
 670        sessions: Arc<Mutex<HashMap<acp::SessionId, Session>>>,
 671        permission_requests: HashMap<acp::ToolCallId, PermissionOptions>,
 672        next_prompt_updates: Arc<Mutex<Vec<acp::SessionUpdate>>>,
 673        supports_load_session: bool,
 674        agent_id: AgentId,
 675        telemetry_id: SharedString,
 676    }
 677
 678    struct Session {
 679        thread: WeakEntity<AcpThread>,
 680        response_tx: Option<oneshot::Sender<acp::StopReason>>,
 681    }
 682
 683    impl Default for StubAgentConnection {
 684        fn default() -> Self {
 685            Self::new()
 686        }
 687    }
 688
 689    impl StubAgentConnection {
 690        pub fn new() -> Self {
 691            Self {
 692                next_prompt_updates: Default::default(),
 693                permission_requests: HashMap::default(),
 694                sessions: Arc::default(),
 695                supports_load_session: false,
 696                agent_id: AgentId::new("stub"),
 697                telemetry_id: "stub".into(),
 698            }
 699        }
 700
 701        pub fn set_next_prompt_updates(&self, updates: Vec<acp::SessionUpdate>) {
 702            *self.next_prompt_updates.lock() = updates;
 703        }
 704
 705        pub fn with_permission_requests(
 706            mut self,
 707            permission_requests: HashMap<acp::ToolCallId, PermissionOptions>,
 708        ) -> Self {
 709            self.permission_requests = permission_requests;
 710            self
 711        }
 712
 713        pub fn with_supports_load_session(mut self, supports_load_session: bool) -> Self {
 714            self.supports_load_session = supports_load_session;
 715            self
 716        }
 717
 718        pub fn with_agent_id(mut self, agent_id: AgentId) -> Self {
 719            self.agent_id = agent_id;
 720            self
 721        }
 722
 723        pub fn with_telemetry_id(mut self, telemetry_id: SharedString) -> Self {
 724            self.telemetry_id = telemetry_id;
 725            self
 726        }
 727
 728        fn create_session(
 729            self: Rc<Self>,
 730            session_id: acp::SessionId,
 731            project: Entity<Project>,
 732            work_dirs: PathList,
 733            title: Option<SharedString>,
 734            cx: &mut gpui::App,
 735        ) -> Entity<AcpThread> {
 736            let action_log = cx.new(|_| ActionLog::new(project.clone()));
 737            let thread = cx.new(|cx| {
 738                AcpThread::new(
 739                    None,
 740                    title,
 741                    Some(work_dirs),
 742                    self.clone(),
 743                    project,
 744                    action_log,
 745                    session_id.clone(),
 746                    watch::Receiver::constant(
 747                        acp::PromptCapabilities::new()
 748                            .image(true)
 749                            .audio(true)
 750                            .embedded_context(true),
 751                    ),
 752                    cx,
 753                )
 754            });
 755            self.sessions.lock().insert(
 756                session_id,
 757                Session {
 758                    thread: thread.downgrade(),
 759                    response_tx: None,
 760                },
 761            );
 762            thread
 763        }
 764
 765        pub fn send_update(
 766            &self,
 767            session_id: acp::SessionId,
 768            update: acp::SessionUpdate,
 769            cx: &mut App,
 770        ) {
 771            assert!(
 772                self.next_prompt_updates.lock().is_empty(),
 773                "Use either send_update or set_next_prompt_updates"
 774            );
 775
 776            self.sessions
 777                .lock()
 778                .get(&session_id)
 779                .unwrap()
 780                .thread
 781                .update(cx, |thread, cx| {
 782                    thread.handle_session_update(update, cx).unwrap();
 783                })
 784                .unwrap();
 785        }
 786
 787        pub fn end_turn(&self, session_id: acp::SessionId, stop_reason: acp::StopReason) {
 788            self.sessions
 789                .lock()
 790                .get_mut(&session_id)
 791                .unwrap()
 792                .response_tx
 793                .take()
 794                .expect("No pending turn")
 795                .send(stop_reason)
 796                .unwrap();
 797        }
 798    }
 799
 800    impl AgentConnection for StubAgentConnection {
 801        fn agent_id(&self) -> AgentId {
 802            self.agent_id.clone()
 803        }
 804
 805        fn telemetry_id(&self) -> SharedString {
 806            self.telemetry_id.clone()
 807        }
 808
 809        fn auth_methods(&self) -> &[acp::AuthMethod] {
 810            &[]
 811        }
 812
 813        fn model_selector(
 814            &self,
 815            _session_id: &acp::SessionId,
 816        ) -> Option<Rc<dyn AgentModelSelector>> {
 817            Some(self.model_selector_impl())
 818        }
 819
 820        fn new_session(
 821            self: Rc<Self>,
 822            project: Entity<Project>,
 823            work_dirs: PathList,
 824            cx: &mut gpui::App,
 825        ) -> Task<gpui::Result<Entity<AcpThread>>> {
 826            static NEXT_SESSION_ID: AtomicUsize = AtomicUsize::new(0);
 827            let session_id =
 828                acp::SessionId::new(NEXT_SESSION_ID.fetch_add(1, Ordering::SeqCst).to_string());
 829            let thread = self.create_session(session_id, project, work_dirs, None, cx);
 830            Task::ready(Ok(thread))
 831        }
 832
 833        fn supports_load_session(&self) -> bool {
 834            self.supports_load_session
 835        }
 836
 837        fn load_session(
 838            self: Rc<Self>,
 839            session_id: acp::SessionId,
 840            project: Entity<Project>,
 841            work_dirs: PathList,
 842            title: Option<SharedString>,
 843            cx: &mut App,
 844        ) -> Task<Result<Entity<AcpThread>>> {
 845            if !self.supports_load_session {
 846                return Task::ready(Err(anyhow::Error::msg("Loading sessions is not supported")));
 847            }
 848
 849            let thread = self.create_session(session_id, project, work_dirs, title, cx);
 850            Task::ready(Ok(thread))
 851        }
 852
 853        fn authenticate(
 854            &self,
 855            _method_id: acp::AuthMethodId,
 856            _cx: &mut App,
 857        ) -> Task<gpui::Result<()>> {
 858            unimplemented!()
 859        }
 860
 861        fn prompt(
 862            &self,
 863            _id: Option<UserMessageId>,
 864            params: acp::PromptRequest,
 865            cx: &mut App,
 866        ) -> Task<gpui::Result<acp::PromptResponse>> {
 867            let mut sessions = self.sessions.lock();
 868            let Session {
 869                thread,
 870                response_tx,
 871            } = sessions.get_mut(&params.session_id).unwrap();
 872            let mut tasks = vec![];
 873            if self.next_prompt_updates.lock().is_empty() {
 874                let (tx, rx) = oneshot::channel();
 875                response_tx.replace(tx);
 876                cx.spawn(async move |_| {
 877                    let stop_reason = rx.await?;
 878                    Ok(acp::PromptResponse::new(stop_reason))
 879                })
 880            } else {
 881                for update in self.next_prompt_updates.lock().drain(..) {
 882                    let thread = thread.clone();
 883                    let update = update.clone();
 884                    let permission_request = if let acp::SessionUpdate::ToolCall(tool_call) =
 885                        &update
 886                        && let Some(options) = self.permission_requests.get(&tool_call.tool_call_id)
 887                    {
 888                        Some((tool_call.clone(), options.clone()))
 889                    } else {
 890                        None
 891                    };
 892                    let task = cx.spawn(async move |cx| {
 893                        if let Some((tool_call, options)) = permission_request {
 894                            thread
 895                                .update(cx, |thread, cx| {
 896                                    thread.request_tool_call_authorization(
 897                                        tool_call.clone().into(),
 898                                        options.clone(),
 899                                        cx,
 900                                    )
 901                                })??
 902                                .await;
 903                        }
 904                        thread.update(cx, |thread, cx| {
 905                            thread.handle_session_update(update.clone(), cx).unwrap();
 906                        })?;
 907                        anyhow::Ok(())
 908                    });
 909                    tasks.push(task);
 910                }
 911
 912                cx.spawn(async move |_| {
 913                    try_join_all(tasks).await?;
 914                    Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
 915                })
 916            }
 917        }
 918
 919        fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) {
 920            if let Some(end_turn_tx) = self
 921                .sessions
 922                .lock()
 923                .get_mut(session_id)
 924                .unwrap()
 925                .response_tx
 926                .take()
 927            {
 928                end_turn_tx.send(acp::StopReason::Cancelled).unwrap();
 929            }
 930        }
 931
 932        fn set_title(
 933            &self,
 934            _session_id: &acp::SessionId,
 935            _cx: &App,
 936        ) -> Option<Rc<dyn AgentSessionSetTitle>> {
 937            Some(Rc::new(StubAgentSessionSetTitle))
 938        }
 939
 940        fn truncate(
 941            &self,
 942            _session_id: &agent_client_protocol::SessionId,
 943            _cx: &App,
 944        ) -> Option<Rc<dyn AgentSessionTruncate>> {
 945            Some(Rc::new(StubAgentSessionEditor))
 946        }
 947
 948        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
 949            self
 950        }
 951    }
 952
 953    struct StubAgentSessionSetTitle;
 954
 955    impl AgentSessionSetTitle for StubAgentSessionSetTitle {
 956        fn run(&self, _title: SharedString, _cx: &mut App) -> Task<Result<()>> {
 957            Task::ready(Ok(()))
 958        }
 959    }
 960
 961    struct StubAgentSessionEditor;
 962
 963    impl AgentSessionTruncate for StubAgentSessionEditor {
 964        fn run(&self, _: UserMessageId, _: &mut App) -> Task<Result<()>> {
 965            Task::ready(Ok(()))
 966        }
 967    }
 968
 969    #[derive(Clone)]
 970    struct StubModelSelector {
 971        selected_model: Arc<Mutex<AgentModelInfo>>,
 972    }
 973
 974    impl StubModelSelector {
 975        fn new() -> Self {
 976            Self {
 977                selected_model: Arc::new(Mutex::new(AgentModelInfo {
 978                    id: acp::ModelId::new("visual-test-model"),
 979                    name: "Visual Test Model".into(),
 980                    description: Some("A stub model for visual testing".into()),
 981                    icon: Some(AgentModelIcon::Named(ui::IconName::ZedAssistant)),
 982                    is_latest: false,
 983                    cost: None,
 984                })),
 985            }
 986        }
 987    }
 988
 989    impl AgentModelSelector for StubModelSelector {
 990        fn list_models(&self, _cx: &mut App) -> Task<Result<AgentModelList>> {
 991            let model = self.selected_model.lock().clone();
 992            Task::ready(Ok(AgentModelList::Flat(vec![model])))
 993        }
 994
 995        fn select_model(&self, model_id: acp::ModelId, _cx: &mut App) -> Task<Result<()>> {
 996            self.selected_model.lock().id = model_id;
 997            Task::ready(Ok(()))
 998        }
 999
1000        fn selected_model(&self, _cx: &mut App) -> Task<Result<AgentModelInfo>> {
1001            Task::ready(Ok(self.selected_model.lock().clone()))
1002        }
1003    }
1004
1005    impl StubAgentConnection {
1006        /// Returns a model selector for this stub connection.
1007        pub fn model_selector_impl(&self) -> Rc<dyn AgentModelSelector> {
1008            Rc::new(StubModelSelector::new())
1009        }
1010    }
1011}
1012
1013#[cfg(feature = "test-support")]
1014pub use test_support::*;