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