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
482/// Pairs a tool's permission pattern with its display name
483///
484/// For example, a pattern of `^cargo\\s+build(\\s|$)` would display as `cargo
485/// build`. It's handy to keep these together rather than trying to derive
486/// one from the other.
487#[derive(Debug, Clone, PartialEq)]
488pub struct PermissionPattern {
489    pub pattern: String,
490    pub display_name: String,
491}
492
493#[derive(Debug, Clone)]
494pub enum PermissionOptions {
495    Flat(Vec<acp::PermissionOption>),
496    Dropdown(Vec<PermissionOptionChoice>),
497    DropdownWithPatterns {
498        choices: Vec<PermissionOptionChoice>,
499        patterns: Vec<PermissionPattern>,
500        tool_name: String,
501    },
502}
503
504impl PermissionOptions {
505    pub fn is_empty(&self) -> bool {
506        match self {
507            PermissionOptions::Flat(options) => options.is_empty(),
508            PermissionOptions::Dropdown(options) => options.is_empty(),
509            PermissionOptions::DropdownWithPatterns { choices, .. } => choices.is_empty(),
510        }
511    }
512
513    pub fn first_option_of_kind(
514        &self,
515        kind: acp::PermissionOptionKind,
516    ) -> Option<&acp::PermissionOption> {
517        match self {
518            PermissionOptions::Flat(options) => options.iter().find(|option| option.kind == kind),
519            PermissionOptions::Dropdown(options) => options.iter().find_map(|choice| {
520                if choice.allow.kind == kind {
521                    Some(&choice.allow)
522                } else if choice.deny.kind == kind {
523                    Some(&choice.deny)
524                } else {
525                    None
526                }
527            }),
528            PermissionOptions::DropdownWithPatterns { choices, .. } => {
529                choices.iter().find_map(|choice| {
530                    if choice.allow.kind == kind {
531                        Some(&choice.allow)
532                    } else if choice.deny.kind == kind {
533                        Some(&choice.deny)
534                    } else {
535                        None
536                    }
537                })
538            }
539        }
540    }
541
542    pub fn allow_once_option_id(&self) -> Option<acp::PermissionOptionId> {
543        self.first_option_of_kind(acp::PermissionOptionKind::AllowOnce)
544            .map(|option| option.option_id.clone())
545    }
546
547    pub fn deny_once_option_id(&self) -> Option<acp::PermissionOptionId> {
548        self.first_option_of_kind(acp::PermissionOptionKind::RejectOnce)
549            .map(|option| option.option_id.clone())
550    }
551}
552
553#[cfg(feature = "test-support")]
554mod test_support {
555    //! Test-only stubs and helpers for acp_thread.
556    //!
557    //! This module is gated by the `test-support` feature and is not included
558    //! in production builds. It provides:
559    //! - `StubAgentConnection` for mocking agent connections in tests
560    //! - `create_test_png_base64` for generating test images
561
562    use std::sync::Arc;
563    use std::sync::atomic::{AtomicUsize, Ordering};
564
565    use action_log::ActionLog;
566    use collections::HashMap;
567    use futures::{channel::oneshot, future::try_join_all};
568    use gpui::{AppContext as _, WeakEntity};
569    use parking_lot::Mutex;
570
571    use super::*;
572
573    /// Creates a PNG image encoded as base64 for testing.
574    ///
575    /// Generates a solid-color PNG of the specified dimensions and returns
576    /// it as a base64-encoded string suitable for use in `ImageContent`.
577    pub fn create_test_png_base64(width: u32, height: u32, color: [u8; 4]) -> String {
578        use image::ImageEncoder as _;
579
580        let mut png_data = Vec::new();
581        {
582            let encoder = image::codecs::png::PngEncoder::new(&mut png_data);
583            let mut pixels = Vec::with_capacity((width * height * 4) as usize);
584            for _ in 0..(width * height) {
585                pixels.extend_from_slice(&color);
586            }
587            encoder
588                .write_image(&pixels, width, height, image::ExtendedColorType::Rgba8)
589                .expect("Failed to encode PNG");
590        }
591
592        use image::EncodableLayout as _;
593        base64::Engine::encode(
594            &base64::engine::general_purpose::STANDARD,
595            png_data.as_bytes(),
596        )
597    }
598
599    #[derive(Clone)]
600    pub struct StubAgentConnection {
601        sessions: Arc<Mutex<HashMap<acp::SessionId, Session>>>,
602        permission_requests: HashMap<acp::ToolCallId, PermissionOptions>,
603        next_prompt_updates: Arc<Mutex<Vec<acp::SessionUpdate>>>,
604        supports_load_session: bool,
605        agent_id: AgentId,
606        telemetry_id: SharedString,
607    }
608
609    struct Session {
610        thread: WeakEntity<AcpThread>,
611        response_tx: Option<oneshot::Sender<acp::StopReason>>,
612    }
613
614    impl Default for StubAgentConnection {
615        fn default() -> Self {
616            Self::new()
617        }
618    }
619
620    impl StubAgentConnection {
621        pub fn new() -> Self {
622            Self {
623                next_prompt_updates: Default::default(),
624                permission_requests: HashMap::default(),
625                sessions: Arc::default(),
626                supports_load_session: false,
627                agent_id: AgentId::new("stub"),
628                telemetry_id: "stub".into(),
629            }
630        }
631
632        pub fn set_next_prompt_updates(&self, updates: Vec<acp::SessionUpdate>) {
633            *self.next_prompt_updates.lock() = updates;
634        }
635
636        pub fn with_permission_requests(
637            mut self,
638            permission_requests: HashMap<acp::ToolCallId, PermissionOptions>,
639        ) -> Self {
640            self.permission_requests = permission_requests;
641            self
642        }
643
644        pub fn with_supports_load_session(mut self, supports_load_session: bool) -> Self {
645            self.supports_load_session = supports_load_session;
646            self
647        }
648
649        pub fn with_agent_id(mut self, agent_id: AgentId) -> Self {
650            self.agent_id = agent_id;
651            self
652        }
653
654        pub fn with_telemetry_id(mut self, telemetry_id: SharedString) -> Self {
655            self.telemetry_id = telemetry_id;
656            self
657        }
658
659        fn create_session(
660            self: Rc<Self>,
661            session_id: acp::SessionId,
662            project: Entity<Project>,
663            work_dirs: PathList,
664            title: Option<SharedString>,
665            cx: &mut gpui::App,
666        ) -> Entity<AcpThread> {
667            let action_log = cx.new(|_| ActionLog::new(project.clone()));
668            let thread_title = title.unwrap_or_else(|| SharedString::new_static("Test"));
669            let thread = cx.new(|cx| {
670                AcpThread::new(
671                    None,
672                    thread_title,
673                    Some(work_dirs),
674                    self.clone(),
675                    project,
676                    action_log,
677                    session_id.clone(),
678                    watch::Receiver::constant(
679                        acp::PromptCapabilities::new()
680                            .image(true)
681                            .audio(true)
682                            .embedded_context(true),
683                    ),
684                    cx,
685                )
686            });
687            self.sessions.lock().insert(
688                session_id,
689                Session {
690                    thread: thread.downgrade(),
691                    response_tx: None,
692                },
693            );
694            thread
695        }
696
697        pub fn send_update(
698            &self,
699            session_id: acp::SessionId,
700            update: acp::SessionUpdate,
701            cx: &mut App,
702        ) {
703            assert!(
704                self.next_prompt_updates.lock().is_empty(),
705                "Use either send_update or set_next_prompt_updates"
706            );
707
708            self.sessions
709                .lock()
710                .get(&session_id)
711                .unwrap()
712                .thread
713                .update(cx, |thread, cx| {
714                    thread.handle_session_update(update, cx).unwrap();
715                })
716                .unwrap();
717        }
718
719        pub fn end_turn(&self, session_id: acp::SessionId, stop_reason: acp::StopReason) {
720            self.sessions
721                .lock()
722                .get_mut(&session_id)
723                .unwrap()
724                .response_tx
725                .take()
726                .expect("No pending turn")
727                .send(stop_reason)
728                .unwrap();
729        }
730    }
731
732    impl AgentConnection for StubAgentConnection {
733        fn agent_id(&self) -> AgentId {
734            self.agent_id.clone()
735        }
736
737        fn telemetry_id(&self) -> SharedString {
738            self.telemetry_id.clone()
739        }
740
741        fn auth_methods(&self) -> &[acp::AuthMethod] {
742            &[]
743        }
744
745        fn model_selector(
746            &self,
747            _session_id: &acp::SessionId,
748        ) -> Option<Rc<dyn AgentModelSelector>> {
749            Some(self.model_selector_impl())
750        }
751
752        fn new_session(
753            self: Rc<Self>,
754            project: Entity<Project>,
755            work_dirs: PathList,
756            cx: &mut gpui::App,
757        ) -> Task<gpui::Result<Entity<AcpThread>>> {
758            static NEXT_SESSION_ID: AtomicUsize = AtomicUsize::new(0);
759            let session_id =
760                acp::SessionId::new(NEXT_SESSION_ID.fetch_add(1, Ordering::SeqCst).to_string());
761            let thread = self.create_session(session_id, project, work_dirs, None, cx);
762            Task::ready(Ok(thread))
763        }
764
765        fn supports_load_session(&self) -> bool {
766            self.supports_load_session
767        }
768
769        fn load_session(
770            self: Rc<Self>,
771            session_id: acp::SessionId,
772            project: Entity<Project>,
773            work_dirs: PathList,
774            title: Option<SharedString>,
775            cx: &mut App,
776        ) -> Task<Result<Entity<AcpThread>>> {
777            if !self.supports_load_session {
778                return Task::ready(Err(anyhow::Error::msg("Loading sessions is not supported")));
779            }
780
781            let thread = self.create_session(session_id, project, work_dirs, title, cx);
782            Task::ready(Ok(thread))
783        }
784
785        fn authenticate(
786            &self,
787            _method_id: acp::AuthMethodId,
788            _cx: &mut App,
789        ) -> Task<gpui::Result<()>> {
790            unimplemented!()
791        }
792
793        fn prompt(
794            &self,
795            _id: Option<UserMessageId>,
796            params: acp::PromptRequest,
797            cx: &mut App,
798        ) -> Task<gpui::Result<acp::PromptResponse>> {
799            let mut sessions = self.sessions.lock();
800            let Session {
801                thread,
802                response_tx,
803            } = sessions.get_mut(&params.session_id).unwrap();
804            let mut tasks = vec![];
805            if self.next_prompt_updates.lock().is_empty() {
806                let (tx, rx) = oneshot::channel();
807                response_tx.replace(tx);
808                cx.spawn(async move |_| {
809                    let stop_reason = rx.await?;
810                    Ok(acp::PromptResponse::new(stop_reason))
811                })
812            } else {
813                for update in self.next_prompt_updates.lock().drain(..) {
814                    let thread = thread.clone();
815                    let update = update.clone();
816                    let permission_request = if let acp::SessionUpdate::ToolCall(tool_call) =
817                        &update
818                        && let Some(options) = self.permission_requests.get(&tool_call.tool_call_id)
819                    {
820                        Some((tool_call.clone(), options.clone()))
821                    } else {
822                        None
823                    };
824                    let task = cx.spawn(async move |cx| {
825                        if let Some((tool_call, options)) = permission_request {
826                            thread
827                                .update(cx, |thread, cx| {
828                                    thread.request_tool_call_authorization(
829                                        tool_call.clone().into(),
830                                        options.clone(),
831                                        cx,
832                                    )
833                                })??
834                                .await;
835                        }
836                        thread.update(cx, |thread, cx| {
837                            thread.handle_session_update(update.clone(), cx).unwrap();
838                        })?;
839                        anyhow::Ok(())
840                    });
841                    tasks.push(task);
842                }
843
844                cx.spawn(async move |_| {
845                    try_join_all(tasks).await?;
846                    Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
847                })
848            }
849        }
850
851        fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) {
852            if let Some(end_turn_tx) = self
853                .sessions
854                .lock()
855                .get_mut(session_id)
856                .unwrap()
857                .response_tx
858                .take()
859            {
860                end_turn_tx.send(acp::StopReason::Cancelled).unwrap();
861            }
862        }
863
864        fn set_title(
865            &self,
866            _session_id: &acp::SessionId,
867            _cx: &App,
868        ) -> Option<Rc<dyn AgentSessionSetTitle>> {
869            Some(Rc::new(StubAgentSessionSetTitle))
870        }
871
872        fn truncate(
873            &self,
874            _session_id: &agent_client_protocol::SessionId,
875            _cx: &App,
876        ) -> Option<Rc<dyn AgentSessionTruncate>> {
877            Some(Rc::new(StubAgentSessionEditor))
878        }
879
880        fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
881            self
882        }
883    }
884
885    struct StubAgentSessionSetTitle;
886
887    impl AgentSessionSetTitle for StubAgentSessionSetTitle {
888        fn run(&self, _title: SharedString, _cx: &mut App) -> Task<Result<()>> {
889            Task::ready(Ok(()))
890        }
891    }
892
893    struct StubAgentSessionEditor;
894
895    impl AgentSessionTruncate for StubAgentSessionEditor {
896        fn run(&self, _: UserMessageId, _: &mut App) -> Task<Result<()>> {
897            Task::ready(Ok(()))
898        }
899    }
900
901    #[derive(Clone)]
902    struct StubModelSelector {
903        selected_model: Arc<Mutex<AgentModelInfo>>,
904    }
905
906    impl StubModelSelector {
907        fn new() -> Self {
908            Self {
909                selected_model: Arc::new(Mutex::new(AgentModelInfo {
910                    id: acp::ModelId::new("visual-test-model"),
911                    name: "Visual Test Model".into(),
912                    description: Some("A stub model for visual testing".into()),
913                    icon: Some(AgentModelIcon::Named(ui::IconName::ZedAssistant)),
914                    is_latest: false,
915                    cost: None,
916                })),
917            }
918        }
919    }
920
921    impl AgentModelSelector for StubModelSelector {
922        fn list_models(&self, _cx: &mut App) -> Task<Result<AgentModelList>> {
923            let model = self.selected_model.lock().clone();
924            Task::ready(Ok(AgentModelList::Flat(vec![model])))
925        }
926
927        fn select_model(&self, model_id: acp::ModelId, _cx: &mut App) -> Task<Result<()>> {
928            self.selected_model.lock().id = model_id;
929            Task::ready(Ok(()))
930        }
931
932        fn selected_model(&self, _cx: &mut App) -> Task<Result<AgentModelInfo>> {
933            Task::ready(Ok(self.selected_model.lock().clone()))
934        }
935    }
936
937    impl StubAgentConnection {
938        /// Returns a model selector for this stub connection.
939        pub fn model_selector_impl(&self) -> Rc<dyn AgentModelSelector> {
940            Rc::new(StubModelSelector::new())
941        }
942    }
943}
944
945#[cfg(feature = "test-support")]
946pub use test_support::*;