1use crate::AcpThread;
2use agent_client_protocol::{self as acp};
3use anyhow::Result;
4use chrono::{DateTime, Utc};
5use collections::IndexMap;
6use gpui::{Entity, SharedString, Task};
7use language_model::LanguageModelProviderId;
8use project::Project;
9use serde::{Deserialize, Serialize};
10use std::{
11 any::Any,
12 error::Error,
13 fmt,
14 path::{Path, PathBuf},
15 rc::Rc,
16 sync::Arc,
17};
18use ui::{App, IconName};
19use uuid::Uuid;
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
22pub struct UserMessageId(Arc<str>);
23
24impl UserMessageId {
25 pub fn new() -> Self {
26 Self(Uuid::new_v4().to_string().into())
27 }
28}
29
30pub trait AgentConnection {
31 fn telemetry_id(&self) -> SharedString;
32
33 fn new_session(
34 self: Rc<Self>,
35 project: Entity<Project>,
36 cwd: &Path,
37 cx: &mut App,
38 ) -> Task<Result<Entity<AcpThread>>>;
39
40 /// Whether this agent supports loading existing sessions.
41 fn supports_load_session(&self) -> bool {
42 false
43 }
44
45 /// Load an existing session by ID.
46 fn load_session(
47 self: Rc<Self>,
48 _session: AgentSessionInfo,
49 _project: Entity<Project>,
50 _cwd: &Path,
51 _cx: &mut App,
52 ) -> Task<Result<Entity<AcpThread>>> {
53 Task::ready(Err(anyhow::Error::msg("Loading sessions is not supported")))
54 }
55
56 /// Whether this agent supports closing existing sessions.
57 fn supports_close_session(&self) -> bool {
58 false
59 }
60
61 /// Close an existing session. Allows the agent to free the session from memory.
62 fn close_session(&self, _session_id: &acp::SessionId, _cx: &mut App) -> Task<Result<()>> {
63 Task::ready(Err(anyhow::Error::msg("Closing sessions is not supported")))
64 }
65
66 /// Whether this agent supports resuming existing sessions without loading history.
67 fn supports_resume_session(&self) -> bool {
68 false
69 }
70
71 /// Resume an existing session by ID without replaying previous messages.
72 fn resume_session(
73 self: Rc<Self>,
74 _session: AgentSessionInfo,
75 _project: Entity<Project>,
76 _cwd: &Path,
77 _cx: &mut App,
78 ) -> Task<Result<Entity<AcpThread>>> {
79 Task::ready(Err(anyhow::Error::msg(
80 "Resuming sessions is not supported",
81 )))
82 }
83
84 /// Whether this agent supports showing session history.
85 fn supports_session_history(&self) -> bool {
86 self.supports_load_session() || self.supports_resume_session()
87 }
88
89 fn auth_methods(&self) -> &[acp::AuthMethod];
90
91 fn authenticate(&self, method: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>>;
92
93 fn prompt(
94 &self,
95 user_message_id: Option<UserMessageId>,
96 params: acp::PromptRequest,
97 cx: &mut App,
98 ) -> Task<Result<acp::PromptResponse>>;
99
100 fn retry(&self, _session_id: &acp::SessionId, _cx: &App) -> Option<Rc<dyn AgentSessionRetry>> {
101 None
102 }
103
104 fn cancel(&self, session_id: &acp::SessionId, cx: &mut App);
105
106 fn truncate(
107 &self,
108 _session_id: &acp::SessionId,
109 _cx: &App,
110 ) -> Option<Rc<dyn AgentSessionTruncate>> {
111 None
112 }
113
114 fn set_title(
115 &self,
116 _session_id: &acp::SessionId,
117 _cx: &App,
118 ) -> Option<Rc<dyn AgentSessionSetTitle>> {
119 None
120 }
121
122 /// Returns this agent as an [Rc<dyn ModelSelector>] if the model selection capability is supported.
123 ///
124 /// If the agent does not support model selection, returns [None].
125 /// This allows sharing the selector in UI components.
126 fn model_selector(&self, _session_id: &acp::SessionId) -> Option<Rc<dyn AgentModelSelector>> {
127 None
128 }
129
130 fn telemetry(&self) -> Option<Rc<dyn AgentTelemetry>> {
131 None
132 }
133
134 fn session_modes(
135 &self,
136 _session_id: &acp::SessionId,
137 _cx: &App,
138 ) -> Option<Rc<dyn AgentSessionModes>> {
139 None
140 }
141
142 fn session_config_options(
143 &self,
144 _session_id: &acp::SessionId,
145 _cx: &App,
146 ) -> Option<Rc<dyn AgentSessionConfigOptions>> {
147 None
148 }
149
150 fn session_list(&self, _cx: &mut App) -> Option<Rc<dyn AgentSessionList>> {
151 None
152 }
153
154 fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
155}
156
157impl dyn AgentConnection {
158 pub fn downcast<T: 'static + AgentConnection + Sized>(self: Rc<Self>) -> Option<Rc<T>> {
159 self.into_any().downcast().ok()
160 }
161}
162
163pub trait AgentSessionTruncate {
164 fn run(&self, message_id: UserMessageId, cx: &mut App) -> Task<Result<()>>;
165}
166
167pub trait AgentSessionRetry {
168 fn run(&self, cx: &mut App) -> Task<Result<acp::PromptResponse>>;
169}
170
171pub trait AgentSessionSetTitle {
172 fn run(&self, title: SharedString, cx: &mut App) -> Task<Result<()>>;
173}
174
175pub trait AgentTelemetry {
176 /// A representation of the current thread state that can be serialized for
177 /// storage with telemetry events.
178 fn thread_data(
179 &self,
180 session_id: &acp::SessionId,
181 cx: &mut App,
182 ) -> Task<Result<serde_json::Value>>;
183}
184
185pub trait AgentSessionModes {
186 fn current_mode(&self) -> acp::SessionModeId;
187
188 fn all_modes(&self) -> Vec<acp::SessionMode>;
189
190 fn set_mode(&self, mode: acp::SessionModeId, cx: &mut App) -> Task<Result<()>>;
191}
192
193pub trait AgentSessionConfigOptions {
194 /// Get all current config options with their state
195 fn config_options(&self) -> Vec<acp::SessionConfigOption>;
196
197 /// Set a config option value
198 /// Returns the full updated list of config options
199 fn set_config_option(
200 &self,
201 config_id: acp::SessionConfigId,
202 value: acp::SessionConfigValueId,
203 cx: &mut App,
204 ) -> Task<Result<Vec<acp::SessionConfigOption>>>;
205
206 /// Whenever the config options are updated the receiver will be notified.
207 /// Optional for agents that don't update their config options dynamically.
208 fn watch(&self, _cx: &mut App) -> Option<watch::Receiver<()>> {
209 None
210 }
211}
212
213#[derive(Debug, Clone, Default)]
214pub struct AgentSessionListRequest {
215 pub cwd: Option<PathBuf>,
216 pub cursor: Option<String>,
217 pub meta: Option<acp::Meta>,
218}
219
220#[derive(Debug, Clone)]
221pub struct AgentSessionListResponse {
222 pub sessions: Vec<AgentSessionInfo>,
223 pub next_cursor: Option<String>,
224 pub meta: Option<acp::Meta>,
225}
226
227impl AgentSessionListResponse {
228 pub fn new(sessions: Vec<AgentSessionInfo>) -> Self {
229 Self {
230 sessions,
231 next_cursor: None,
232 meta: None,
233 }
234 }
235}
236
237#[derive(Debug, Clone, PartialEq)]
238pub struct AgentSessionInfo {
239 pub session_id: acp::SessionId,
240 pub cwd: Option<PathBuf>,
241 pub title: Option<SharedString>,
242 pub updated_at: Option<DateTime<Utc>>,
243 pub meta: Option<acp::Meta>,
244}
245
246impl AgentSessionInfo {
247 pub fn new(session_id: impl Into<acp::SessionId>) -> Self {
248 Self {
249 session_id: session_id.into(),
250 cwd: None,
251 title: None,
252 updated_at: None,
253 meta: None,
254 }
255 }
256}
257
258#[derive(Debug, Clone)]
259pub enum SessionListUpdate {
260 Refresh,
261 SessionInfo {
262 session_id: acp::SessionId,
263 update: acp::SessionInfoUpdate,
264 },
265}
266
267pub trait AgentSessionList {
268 fn list_sessions(
269 &self,
270 request: AgentSessionListRequest,
271 cx: &mut App,
272 ) -> Task<Result<AgentSessionListResponse>>;
273
274 fn supports_delete(&self) -> bool {
275 false
276 }
277
278 fn delete_session(&self, _session_id: &acp::SessionId, _cx: &mut App) -> Task<Result<()>> {
279 Task::ready(Err(anyhow::anyhow!("delete_session not supported")))
280 }
281
282 fn delete_sessions(&self, _cx: &mut App) -> Task<Result<()>> {
283 Task::ready(Err(anyhow::anyhow!("delete_sessions not supported")))
284 }
285
286 fn watch(&self, _cx: &mut App) -> Option<smol::channel::Receiver<SessionListUpdate>> {
287 None
288 }
289
290 fn notify_refresh(&self) {}
291
292 fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
293}
294
295impl dyn AgentSessionList {
296 pub fn downcast<T: 'static + AgentSessionList + Sized>(self: Rc<Self>) -> Option<Rc<T>> {
297 self.into_any().downcast().ok()
298 }
299}
300
301#[derive(Debug)]
302pub struct AuthRequired {
303 pub description: Option<String>,
304 pub provider_id: Option<LanguageModelProviderId>,
305}
306
307impl AuthRequired {
308 pub fn new() -> Self {
309 Self {
310 description: None,
311 provider_id: None,
312 }
313 }
314
315 pub fn with_description(mut self, description: String) -> Self {
316 self.description = Some(description);
317 self
318 }
319
320 pub fn with_language_model_provider(mut self, provider_id: LanguageModelProviderId) -> Self {
321 self.provider_id = Some(provider_id);
322 self
323 }
324}
325
326impl Error for AuthRequired {}
327impl fmt::Display for AuthRequired {
328 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
329 write!(f, "Authentication required")
330 }
331}
332
333/// Trait for agents that support listing, selecting, and querying language models.
334///
335/// This is an optional capability; agents indicate support via [AgentConnection::model_selector].
336pub trait AgentModelSelector: 'static {
337 /// Lists all available language models for this agent.
338 ///
339 /// # Parameters
340 /// - `cx`: The GPUI app context for async operations and global access.
341 ///
342 /// # Returns
343 /// A task resolving to the list of models or an error (e.g., if no models are configured).
344 fn list_models(&self, cx: &mut App) -> Task<Result<AgentModelList>>;
345
346 /// Selects a model for a specific session (thread).
347 ///
348 /// This sets the default model for future interactions in the session.
349 /// If the session doesn't exist or the model is invalid, it returns an error.
350 ///
351 /// # Parameters
352 /// - `model`: The model to select (should be one from [list_models]).
353 /// - `cx`: The GPUI app context.
354 ///
355 /// # Returns
356 /// A task resolving to `Ok(())` on success or an error.
357 fn select_model(&self, model_id: acp::ModelId, cx: &mut App) -> Task<Result<()>>;
358
359 /// Retrieves the currently selected model for a specific session (thread).
360 ///
361 /// # Parameters
362 /// - `cx`: The GPUI app context.
363 ///
364 /// # Returns
365 /// A task resolving to the selected model (always set) or an error (e.g., session not found).
366 fn selected_model(&self, cx: &mut App) -> Task<Result<AgentModelInfo>>;
367
368 /// Whenever the model list is updated the receiver will be notified.
369 /// Optional for agents that don't update their model list.
370 fn watch(&self, _cx: &mut App) -> Option<watch::Receiver<()>> {
371 None
372 }
373
374 /// Returns whether the model picker should render a footer.
375 fn should_render_footer(&self) -> bool {
376 false
377 }
378}
379
380/// Icon for a model in the model selector.
381#[derive(Debug, Clone, PartialEq, Eq)]
382pub enum AgentModelIcon {
383 /// A built-in icon from Zed's icon set.
384 Named(IconName),
385 /// Path to a custom SVG icon file.
386 Path(SharedString),
387}
388
389#[derive(Debug, Clone, PartialEq, Eq)]
390pub struct AgentModelInfo {
391 pub id: acp::ModelId,
392 pub name: SharedString,
393 pub description: Option<SharedString>,
394 pub icon: Option<AgentModelIcon>,
395 pub is_latest: bool,
396 pub cost: Option<SharedString>,
397}
398
399impl From<acp::ModelInfo> for AgentModelInfo {
400 fn from(info: acp::ModelInfo) -> Self {
401 Self {
402 id: info.model_id,
403 name: info.name.into(),
404 description: info.description.map(|desc| desc.into()),
405 icon: None,
406 is_latest: false,
407 cost: None,
408 }
409 }
410}
411
412#[derive(Debug, Clone, PartialEq, Eq, Hash)]
413pub struct AgentModelGroupName(pub SharedString);
414
415#[derive(Debug, Clone)]
416pub enum AgentModelList {
417 Flat(Vec<AgentModelInfo>),
418 Grouped(IndexMap<AgentModelGroupName, Vec<AgentModelInfo>>),
419}
420
421impl AgentModelList {
422 pub fn is_empty(&self) -> bool {
423 match self {
424 AgentModelList::Flat(models) => models.is_empty(),
425 AgentModelList::Grouped(groups) => groups.is_empty(),
426 }
427 }
428
429 pub fn is_flat(&self) -> bool {
430 matches!(self, AgentModelList::Flat(_))
431 }
432}
433
434#[derive(Debug, Clone)]
435pub struct PermissionOptionChoice {
436 pub allow: acp::PermissionOption,
437 pub deny: acp::PermissionOption,
438}
439
440impl PermissionOptionChoice {
441 pub fn label(&self) -> SharedString {
442 self.allow.name.clone().into()
443 }
444}
445
446#[derive(Debug, Clone)]
447pub enum PermissionOptions {
448 Flat(Vec<acp::PermissionOption>),
449 Dropdown(Vec<PermissionOptionChoice>),
450}
451
452impl PermissionOptions {
453 pub fn is_empty(&self) -> bool {
454 match self {
455 PermissionOptions::Flat(options) => options.is_empty(),
456 PermissionOptions::Dropdown(options) => options.is_empty(),
457 }
458 }
459
460 pub fn first_option_of_kind(
461 &self,
462 kind: acp::PermissionOptionKind,
463 ) -> Option<&acp::PermissionOption> {
464 match self {
465 PermissionOptions::Flat(options) => options.iter().find(|option| option.kind == kind),
466 PermissionOptions::Dropdown(options) => options.iter().find_map(|choice| {
467 if choice.allow.kind == kind {
468 Some(&choice.allow)
469 } else if choice.deny.kind == kind {
470 Some(&choice.deny)
471 } else {
472 None
473 }
474 }),
475 }
476 }
477
478 pub fn allow_once_option_id(&self) -> Option<acp::PermissionOptionId> {
479 self.first_option_of_kind(acp::PermissionOptionKind::AllowOnce)
480 .map(|option| option.option_id.clone())
481 }
482
483 pub fn deny_once_option_id(&self) -> Option<acp::PermissionOptionId> {
484 self.first_option_of_kind(acp::PermissionOptionKind::RejectOnce)
485 .map(|option| option.option_id.clone())
486 }
487}
488
489#[cfg(feature = "test-support")]
490mod test_support {
491 //! Test-only stubs and helpers for acp_thread.
492 //!
493 //! This module is gated by the `test-support` feature and is not included
494 //! in production builds. It provides:
495 //! - `StubAgentConnection` for mocking agent connections in tests
496 //! - `create_test_png_base64` for generating test images
497
498 use std::sync::Arc;
499
500 use action_log::ActionLog;
501 use collections::HashMap;
502 use futures::{channel::oneshot, future::try_join_all};
503 use gpui::{AppContext as _, WeakEntity};
504 use parking_lot::Mutex;
505
506 use super::*;
507
508 /// Creates a PNG image encoded as base64 for testing.
509 ///
510 /// Generates a solid-color PNG of the specified dimensions and returns
511 /// it as a base64-encoded string suitable for use in `ImageContent`.
512 pub fn create_test_png_base64(width: u32, height: u32, color: [u8; 4]) -> String {
513 use image::ImageEncoder as _;
514
515 let mut png_data = Vec::new();
516 {
517 let encoder = image::codecs::png::PngEncoder::new(&mut png_data);
518 let mut pixels = Vec::with_capacity((width * height * 4) as usize);
519 for _ in 0..(width * height) {
520 pixels.extend_from_slice(&color);
521 }
522 encoder
523 .write_image(&pixels, width, height, image::ExtendedColorType::Rgba8)
524 .expect("Failed to encode PNG");
525 }
526
527 use image::EncodableLayout as _;
528 base64::Engine::encode(
529 &base64::engine::general_purpose::STANDARD,
530 png_data.as_bytes(),
531 )
532 }
533
534 #[derive(Clone, Default)]
535 pub struct StubAgentConnection {
536 sessions: Arc<Mutex<HashMap<acp::SessionId, Session>>>,
537 permission_requests: HashMap<acp::ToolCallId, PermissionOptions>,
538 next_prompt_updates: Arc<Mutex<Vec<acp::SessionUpdate>>>,
539 }
540
541 struct Session {
542 thread: WeakEntity<AcpThread>,
543 response_tx: Option<oneshot::Sender<acp::StopReason>>,
544 }
545
546 impl StubAgentConnection {
547 pub fn new() -> Self {
548 Self {
549 next_prompt_updates: Default::default(),
550 permission_requests: HashMap::default(),
551 sessions: Arc::default(),
552 }
553 }
554
555 pub fn set_next_prompt_updates(&self, updates: Vec<acp::SessionUpdate>) {
556 *self.next_prompt_updates.lock() = updates;
557 }
558
559 pub fn with_permission_requests(
560 mut self,
561 permission_requests: HashMap<acp::ToolCallId, PermissionOptions>,
562 ) -> Self {
563 self.permission_requests = permission_requests;
564 self
565 }
566
567 pub fn send_update(
568 &self,
569 session_id: acp::SessionId,
570 update: acp::SessionUpdate,
571 cx: &mut App,
572 ) {
573 assert!(
574 self.next_prompt_updates.lock().is_empty(),
575 "Use either send_update or set_next_prompt_updates"
576 );
577
578 self.sessions
579 .lock()
580 .get(&session_id)
581 .unwrap()
582 .thread
583 .update(cx, |thread, cx| {
584 thread.handle_session_update(update, cx).unwrap();
585 })
586 .unwrap();
587 }
588
589 pub fn end_turn(&self, session_id: acp::SessionId, stop_reason: acp::StopReason) {
590 self.sessions
591 .lock()
592 .get_mut(&session_id)
593 .unwrap()
594 .response_tx
595 .take()
596 .expect("No pending turn")
597 .send(stop_reason)
598 .unwrap();
599 }
600 }
601
602 impl AgentConnection for StubAgentConnection {
603 fn telemetry_id(&self) -> SharedString {
604 "stub".into()
605 }
606
607 fn auth_methods(&self) -> &[acp::AuthMethod] {
608 &[]
609 }
610
611 fn model_selector(
612 &self,
613 _session_id: &acp::SessionId,
614 ) -> Option<Rc<dyn AgentModelSelector>> {
615 Some(self.model_selector_impl())
616 }
617
618 fn new_session(
619 self: Rc<Self>,
620 project: Entity<Project>,
621 _cwd: &Path,
622 cx: &mut gpui::App,
623 ) -> Task<gpui::Result<Entity<AcpThread>>> {
624 let session_id = acp::SessionId::new(self.sessions.lock().len().to_string());
625 let action_log = cx.new(|_| ActionLog::new(project.clone()));
626 let thread = cx.new(|cx| {
627 AcpThread::new(
628 None,
629 "Test",
630 self.clone(),
631 project,
632 action_log,
633 session_id.clone(),
634 watch::Receiver::constant(
635 acp::PromptCapabilities::new()
636 .image(true)
637 .audio(true)
638 .embedded_context(true),
639 ),
640 cx,
641 )
642 });
643 self.sessions.lock().insert(
644 session_id,
645 Session {
646 thread: thread.downgrade(),
647 response_tx: None,
648 },
649 );
650 Task::ready(Ok(thread))
651 }
652
653 fn authenticate(
654 &self,
655 _method_id: acp::AuthMethodId,
656 _cx: &mut App,
657 ) -> Task<gpui::Result<()>> {
658 unimplemented!()
659 }
660
661 fn prompt(
662 &self,
663 _id: Option<UserMessageId>,
664 params: acp::PromptRequest,
665 cx: &mut App,
666 ) -> Task<gpui::Result<acp::PromptResponse>> {
667 let mut sessions = self.sessions.lock();
668 let Session {
669 thread,
670 response_tx,
671 } = sessions.get_mut(¶ms.session_id).unwrap();
672 let mut tasks = vec![];
673 if self.next_prompt_updates.lock().is_empty() {
674 let (tx, rx) = oneshot::channel();
675 response_tx.replace(tx);
676 cx.spawn(async move |_| {
677 let stop_reason = rx.await?;
678 Ok(acp::PromptResponse::new(stop_reason))
679 })
680 } else {
681 for update in self.next_prompt_updates.lock().drain(..) {
682 let thread = thread.clone();
683 let update = update.clone();
684 let permission_request = if let acp::SessionUpdate::ToolCall(tool_call) =
685 &update
686 && let Some(options) = self.permission_requests.get(&tool_call.tool_call_id)
687 {
688 Some((tool_call.clone(), options.clone()))
689 } else {
690 None
691 };
692 let task = cx.spawn(async move |cx| {
693 if let Some((tool_call, options)) = permission_request {
694 thread
695 .update(cx, |thread, cx| {
696 thread.request_tool_call_authorization(
697 tool_call.clone().into(),
698 options.clone(),
699 cx,
700 )
701 })??
702 .await;
703 }
704 thread.update(cx, |thread, cx| {
705 thread.handle_session_update(update.clone(), cx).unwrap();
706 })?;
707 anyhow::Ok(())
708 });
709 tasks.push(task);
710 }
711
712 cx.spawn(async move |_| {
713 try_join_all(tasks).await?;
714 Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
715 })
716 }
717 }
718
719 fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) {
720 if let Some(end_turn_tx) = self
721 .sessions
722 .lock()
723 .get_mut(session_id)
724 .unwrap()
725 .response_tx
726 .take()
727 {
728 end_turn_tx.send(acp::StopReason::Cancelled).unwrap();
729 }
730 }
731
732 fn set_title(
733 &self,
734 _session_id: &acp::SessionId,
735 _cx: &App,
736 ) -> Option<Rc<dyn AgentSessionSetTitle>> {
737 Some(Rc::new(StubAgentSessionSetTitle))
738 }
739
740 fn truncate(
741 &self,
742 _session_id: &agent_client_protocol::SessionId,
743 _cx: &App,
744 ) -> Option<Rc<dyn AgentSessionTruncate>> {
745 Some(Rc::new(StubAgentSessionEditor))
746 }
747
748 fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
749 self
750 }
751 }
752
753 struct StubAgentSessionSetTitle;
754
755 impl AgentSessionSetTitle for StubAgentSessionSetTitle {
756 fn run(&self, _title: SharedString, _cx: &mut App) -> Task<Result<()>> {
757 Task::ready(Ok(()))
758 }
759 }
760
761 struct StubAgentSessionEditor;
762
763 impl AgentSessionTruncate for StubAgentSessionEditor {
764 fn run(&self, _: UserMessageId, _: &mut App) -> Task<Result<()>> {
765 Task::ready(Ok(()))
766 }
767 }
768
769 #[derive(Clone)]
770 struct StubModelSelector {
771 selected_model: Arc<Mutex<AgentModelInfo>>,
772 }
773
774 impl StubModelSelector {
775 fn new() -> Self {
776 Self {
777 selected_model: Arc::new(Mutex::new(AgentModelInfo {
778 id: acp::ModelId::new("visual-test-model"),
779 name: "Visual Test Model".into(),
780 description: Some("A stub model for visual testing".into()),
781 icon: Some(AgentModelIcon::Named(ui::IconName::ZedAssistant)),
782 is_latest: false,
783 cost: None,
784 })),
785 }
786 }
787 }
788
789 impl AgentModelSelector for StubModelSelector {
790 fn list_models(&self, _cx: &mut App) -> Task<Result<AgentModelList>> {
791 let model = self.selected_model.lock().clone();
792 Task::ready(Ok(AgentModelList::Flat(vec![model])))
793 }
794
795 fn select_model(&self, model_id: acp::ModelId, _cx: &mut App) -> Task<Result<()>> {
796 self.selected_model.lock().id = model_id;
797 Task::ready(Ok(()))
798 }
799
800 fn selected_model(&self, _cx: &mut App) -> Task<Result<AgentModelInfo>> {
801 Task::ready(Ok(self.selected_model.lock().clone()))
802 }
803 }
804
805 impl StubAgentConnection {
806 /// Returns a model selector for this stub connection.
807 pub fn model_selector_impl(&self) -> Rc<dyn AgentModelSelector> {
808 Rc::new(StubModelSelector::new())
809 }
810 }
811}
812
813#[cfg(feature = "test-support")]
814pub use test_support::*;