1use anyhow::Result;
2use async_trait::async_trait;
3use vtcode_core::core::interfaces::acp::{AcpClientAdapter, AcpLaunchParams};
4
5mod agent;
6pub(crate) mod constants;
7mod helpers;
8mod session;
9mod types;
10
11pub(crate) use agent::ZedAgent;
12use session::run_acp_agent;
13
14#[derive(Debug, Default, Clone, Copy)]
15pub struct ZedAcpAdapter;
16
17#[async_trait(?Send)]
18impl AcpClientAdapter for ZedAcpAdapter {
19 async fn serve(&self, params: AcpLaunchParams<'_>) -> Result<()> {
20 run_acp_agent(
21 params.agent_config,
22 params.runtime_config,
23 Some("Zed".to_string()),
24 )
25 .await
26 }
27}
28
29#[derive(Debug, Default, Clone, Copy)]
30pub struct StandardAcpAdapter;
31
32#[async_trait(?Send)]
33impl AcpClientAdapter for StandardAcpAdapter {
34 async fn serve(&self, params: AcpLaunchParams<'_>) -> Result<()> {
35 run_acp_agent(params.agent_config, params.runtime_config, None).await
36 }
37}
38
39#[cfg(test)]
40mod tests {
41 use super::*;
42 use crate::tooling::{
43 TOOL_LIST_FILES_ITEMS_KEY, TOOL_LIST_FILES_RESULT_KEY, TOOL_LIST_FILES_URI_ARG,
44 };
45 use crate::zed::helpers::{
46 SESSION_CONFIG_MODE_ID, SESSION_CONFIG_MODEL_ID, SESSION_CONFIG_PROVIDER_ID,
47 SESSION_CONFIG_THOUGHT_LEVEL_ID,
48 };
49 use crate::zed::types::NotificationEnvelope;
50 use agent_client_protocol::{
51 Agent, LoadSessionRequest, NewSessionRequest, SessionConfigKind, SessionModeId,
52 SetSessionConfigOptionRequest, ToolCallStatus,
53 };
54 use assert_fs::TempDir;
55 use serde_json::{Value, json};
56 use std::collections::BTreeMap;
57 use std::path::Path;
58 use tokio::fs;
59 use tokio::sync::mpsc;
60 use vtcode_core::config::core::PromptCachingConfig;
61 use vtcode_core::config::models::{ModelId, Provider};
62 use vtcode_core::config::types::{
63 AgentConfig as CoreAgentConfig, ModelSelectionSource, ReasoningEffortLevel,
64 UiSurfacePreference,
65 };
66 use vtcode_core::config::{AgentClientProtocolZedConfig, CommandsConfig, ToolsConfig};
67 use vtcode_core::core::agent::snapshots::{
68 DEFAULT_CHECKPOINTS_ENABLED, DEFAULT_MAX_AGE_DAYS, DEFAULT_MAX_SNAPSHOTS,
69 };
70 use vtcode_core::core::interfaces::SessionMode;
71
72 async fn build_agent(workspace: &Path) -> ZedAgent {
73 let core_config = CoreAgentConfig {
74 model: "gpt-5.4".to_string(),
75 api_key: String::new(),
76 provider: "openai".to_string(),
77 api_key_env: "TEST_API_KEY".to_string(),
78 workspace: workspace.to_path_buf(),
79 verbose: false,
80 quiet: false,
81 theme: "test".to_string(),
82 reasoning_effort: ReasoningEffortLevel::Low,
83 ui_surface: UiSurfacePreference::default(),
84 prompt_cache: PromptCachingConfig::default(),
85 model_source: ModelSelectionSource::WorkspaceConfig,
86 custom_api_keys: BTreeMap::new(),
87 checkpointing_enabled: DEFAULT_CHECKPOINTS_ENABLED,
88 checkpointing_storage_dir: None,
89 checkpointing_max_snapshots: DEFAULT_MAX_SNAPSHOTS,
90 checkpointing_max_age_days: Some(DEFAULT_MAX_AGE_DAYS),
91 max_conversation_turns: 1000,
92 model_behavior: None,
93 openai_chatgpt_auth: None,
94 };
95
96 let mut zed_config = AgentClientProtocolZedConfig::default();
97 zed_config.tools.list_files = true;
98 zed_config.tools.read_file = false;
99
100 let tools_config = ToolsConfig::default();
101 let (tx, mut rx) = mpsc::unbounded_channel::<NotificationEnvelope>();
102 tokio::spawn(async move {
104 while let Some(envelope) = rx.recv().await {
105 let _ = envelope.completion.send(());
106 }
107 });
108
109 ZedAgent::new(
110 core_config,
111 zed_config,
112 tools_config,
113 CommandsConfig::default(),
114 String::new(),
115 tx,
116 Some("Zed".to_string()),
117 )
118 .await
119 }
120
121 fn list_items_from_payload(payload: &Value) -> Vec<Value> {
122 payload
123 .get(TOOL_LIST_FILES_RESULT_KEY)
124 .and_then(Value::as_object)
125 .and_then(|result| result.get(TOOL_LIST_FILES_ITEMS_KEY))
126 .and_then(Value::as_array)
127 .cloned()
128 .unwrap_or_default()
129 }
130
131 #[tokio::test]
132 async fn run_list_files_defaults_to_workspace_root() {
133 let temp = TempDir::new().unwrap();
134 let subdir = temp.path().join("src");
135 fs::create_dir(&subdir).await.unwrap();
136 let file_path = subdir.join("example.txt");
137 fs::write(&file_path, "hello").await.unwrap();
138
139 let agent = build_agent(temp.path()).await;
140 let report = agent.run_list_files(&json!({"path": "src"})).await.unwrap();
141
142 assert!(matches!(report.status, ToolCallStatus::Completed));
143 let payload = report.raw_output.unwrap();
144 let items = list_items_from_payload(&payload);
145 assert!(items.iter().any(|item| {
146 item.get("name")
147 .and_then(Value::as_str)
148 .map(|name| name == "example.txt")
149 .unwrap_or(false)
150 }));
151 }
152
153 #[tokio::test]
154 async fn run_list_files_accepts_uri_argument() {
155 let temp = TempDir::new().unwrap();
156 let nested = temp.path().join("nested");
157 fs::create_dir_all(&nested).await.unwrap();
158 let inner = nested.join("inner.txt");
159 fs::write(&inner, "data").await.unwrap();
160
161 let agent = build_agent(temp.path()).await;
162 let uri = format!("file://{}", nested.to_string_lossy());
163 let report = agent
164 .run_list_files(&json!({ TOOL_LIST_FILES_URI_ARG: uri }))
165 .await
166 .unwrap();
167
168 assert!(matches!(report.status, ToolCallStatus::Completed));
169 let payload = report.raw_output.unwrap();
170 let items = list_items_from_payload(&payload);
171 assert!(items.iter().any(|item| {
172 item.get("path")
173 .and_then(Value::as_str)
174 .map(|path| path.contains("inner.txt"))
175 .unwrap_or(false)
176 }));
177 }
178
179 #[tokio::test]
180 async fn load_session_returns_existing_session_state() {
181 let temp = TempDir::new().unwrap();
182 let agent = build_agent(temp.path()).await;
183 let session_id = agent.register_session();
184
185 {
187 let session = agent.session_handle(&session_id).unwrap();
188 let mut data = session.data.borrow_mut();
189 data.current_mode = SessionMode::Architect;
190 data.reasoning_effort = ReasoningEffortLevel::High;
191 }
192
193 let args = LoadSessionRequest::new(session_id, temp.path());
194 let response = agent.load_session(args).await.unwrap();
195
196 assert_eq!(
197 response.modes.unwrap().current_mode_id,
198 SessionModeId::new("architect")
199 );
200 let config_options = response.config_options.unwrap();
201 assert_eq!(config_options.len(), 4);
202 assert!(config_options.iter().any(|option| {
203 option.id == crate::acp::SessionConfigId::new(SESSION_CONFIG_MODE_ID)
204 && matches!(
205 &option.kind,
206 SessionConfigKind::Select(select)
207 if select.current_value
208 == crate::acp::SessionConfigValueId::new("architect")
209 )
210 }));
211 assert!(config_options.iter().any(|option| {
212 option.id == crate::acp::SessionConfigId::new(SESSION_CONFIG_THOUGHT_LEVEL_ID)
213 && matches!(
214 &option.kind,
215 SessionConfigKind::Select(select)
216 if select.current_value
217 == crate::acp::SessionConfigValueId::new("high")
218 )
219 }));
220 assert!(config_options.iter().any(|option| {
221 option.id == crate::acp::SessionConfigId::new(SESSION_CONFIG_PROVIDER_ID)
222 && matches!(
223 &option.kind,
224 SessionConfigKind::Select(select)
225 if select.current_value
226 == crate::acp::SessionConfigValueId::new("openai")
227 )
228 }));
229 assert!(config_options.iter().any(|option| {
230 option.id == crate::acp::SessionConfigId::new(SESSION_CONFIG_MODEL_ID)
231 && matches!(
232 &option.kind,
233 SessionConfigKind::Select(select)
234 if select.current_value
235 == crate::acp::SessionConfigValueId::new("gpt-5.4")
236 )
237 }));
238 }
239
240 #[tokio::test]
241 async fn new_session_returns_config_options() {
242 let temp = TempDir::new().unwrap();
243 let agent = build_agent(temp.path()).await;
244
245 let response = agent
246 .new_session(NewSessionRequest::new(temp.path()))
247 .await
248 .unwrap();
249
250 assert_eq!(
251 response.modes.unwrap().current_mode_id,
252 SessionModeId::new("code")
253 );
254 let config_options = response.config_options.unwrap();
255 assert_eq!(config_options.len(), 4);
256 assert!(config_options.iter().any(|option| {
257 option.id == crate::acp::SessionConfigId::new(SESSION_CONFIG_MODE_ID)
258 && matches!(
259 &option.kind,
260 SessionConfigKind::Select(select)
261 if select.current_value
262 == crate::acp::SessionConfigValueId::new("code")
263 )
264 }));
265 assert!(config_options.iter().any(|option| {
266 option.id == crate::acp::SessionConfigId::new(SESSION_CONFIG_THOUGHT_LEVEL_ID)
267 && matches!(
268 &option.kind,
269 SessionConfigKind::Select(select)
270 if select.current_value
271 == crate::acp::SessionConfigValueId::new("low")
272 )
273 }));
274 assert!(config_options.iter().any(|option| {
275 option.id == crate::acp::SessionConfigId::new(SESSION_CONFIG_PROVIDER_ID)
276 && matches!(
277 &option.kind,
278 SessionConfigKind::Select(select)
279 if select.current_value
280 == crate::acp::SessionConfigValueId::new("openai")
281 )
282 }));
283 assert!(config_options.iter().any(|option| {
284 option.id == crate::acp::SessionConfigId::new(SESSION_CONFIG_MODEL_ID)
285 && matches!(
286 &option.kind,
287 SessionConfigKind::Select(select)
288 if select.current_value
289 == crate::acp::SessionConfigValueId::new("gpt-5.4")
290 )
291 }));
292 }
293
294 #[tokio::test]
295 async fn set_session_config_option_updates_mode() {
296 let temp = TempDir::new().unwrap();
297 let agent = build_agent(temp.path()).await;
298 let session_id = agent.register_session();
299
300 let response = agent
301 .set_session_config_option(SetSessionConfigOptionRequest::new(
302 session_id.clone(),
303 SESSION_CONFIG_MODE_ID,
304 "architect",
305 ))
306 .await
307 .unwrap();
308
309 let session = agent.session_handle(&session_id).unwrap();
310 assert_eq!(session.data.borrow().current_mode, SessionMode::Architect);
311 assert!(response.config_options.iter().any(|option| {
312 option.id == crate::acp::SessionConfigId::new(SESSION_CONFIG_MODE_ID)
313 && matches!(
314 &option.kind,
315 SessionConfigKind::Select(select)
316 if select.current_value
317 == crate::acp::SessionConfigValueId::new("architect")
318 )
319 }));
320 }
321
322 #[tokio::test]
323 async fn set_session_config_option_updates_reasoning_effort() {
324 let temp = TempDir::new().unwrap();
325 let agent = build_agent(temp.path()).await;
326 let session_id = agent.register_session();
327
328 let response = agent
329 .set_session_config_option(SetSessionConfigOptionRequest::new(
330 session_id.clone(),
331 SESSION_CONFIG_THOUGHT_LEVEL_ID,
332 "xhigh",
333 ))
334 .await
335 .unwrap();
336
337 let session = agent.session_handle(&session_id).unwrap();
338 assert_eq!(
339 session.data.borrow().reasoning_effort,
340 ReasoningEffortLevel::XHigh
341 );
342 assert!(response.config_options.iter().any(|option| {
343 option.id == crate::acp::SessionConfigId::new(SESSION_CONFIG_THOUGHT_LEVEL_ID)
344 && matches!(
345 &option.kind,
346 SessionConfigKind::Select(select)
347 if select.current_value
348 == crate::acp::SessionConfigValueId::new("xhigh")
349 )
350 }));
351 }
352
353 #[tokio::test]
354 async fn set_session_config_option_updates_provider_and_auto_switches_model() {
355 let temp = TempDir::new().unwrap();
356 let agent = build_agent(temp.path()).await;
357 let session_id = agent.register_session();
358 let anthropic_default = ModelId::default_single_for_provider(Provider::Anthropic).as_str();
359
360 let response = agent
361 .set_session_config_option(SetSessionConfigOptionRequest::new(
362 session_id.clone(),
363 SESSION_CONFIG_PROVIDER_ID,
364 "anthropic",
365 ))
366 .await
367 .unwrap();
368
369 let session = agent.session_handle(&session_id).unwrap();
370 assert_eq!(session.data.borrow().provider, "anthropic");
371 assert_eq!(session.data.borrow().model, anthropic_default);
372 assert!(response.config_options.iter().any(|option| {
373 option.id == crate::acp::SessionConfigId::new(SESSION_CONFIG_PROVIDER_ID)
374 && matches!(
375 &option.kind,
376 SessionConfigKind::Select(select)
377 if select.current_value
378 == crate::acp::SessionConfigValueId::new("anthropic")
379 )
380 }));
381 assert!(response.config_options.iter().any(|option| {
382 option.id == crate::acp::SessionConfigId::new(SESSION_CONFIG_MODEL_ID)
383 && matches!(
384 &option.kind,
385 SessionConfigKind::Select(select)
386 if select.current_value
387 == crate::acp::SessionConfigValueId::new(anthropic_default)
388 )
389 }));
390 }
391
392 #[tokio::test]
393 async fn set_session_config_option_updates_model_for_provider() {
394 let temp = TempDir::new().unwrap();
395 let agent = build_agent(temp.path()).await;
396 let session_id = agent.register_session();
397
398 let response = agent
399 .set_session_config_option(SetSessionConfigOptionRequest::new(
400 session_id.clone(),
401 SESSION_CONFIG_MODEL_ID,
402 "gpt-5.4-mini",
403 ))
404 .await
405 .unwrap();
406
407 let session = agent.session_handle(&session_id).unwrap();
408 assert_eq!(session.data.borrow().provider, "openai");
409 assert_eq!(session.data.borrow().model, "gpt-5.4-mini");
410 assert!(response.config_options.iter().any(|option| {
411 option.id == crate::acp::SessionConfigId::new(SESSION_CONFIG_MODEL_ID)
412 && matches!(
413 &option.kind,
414 SessionConfigKind::Select(select)
415 if select.current_value
416 == crate::acp::SessionConfigValueId::new("gpt-5.4-mini")
417 )
418 }));
419 }
420
421 #[tokio::test]
422 async fn run_switch_mode_updates_session_mode() {
423 let temp = TempDir::new().unwrap();
424 let agent = build_agent(temp.path()).await;
425 let session_id = agent.register_session();
426
427 {
429 let session = agent.session_handle(&session_id).unwrap();
430 assert_eq!(session.data.borrow().current_mode, SessionMode::Code);
431 }
432
433 let args = json!({ "mode_id": "architect" });
435 let report = agent.run_switch_mode(&session_id, &args).await.unwrap();
436
437 assert!(matches!(report.status, ToolCallStatus::Completed));
438
439 {
441 let session = agent.session_handle(&session_id).unwrap();
442 assert_eq!(session.data.borrow().current_mode, SessionMode::Architect);
443 }
444 }
445}