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