1// Translates old acp agents into the new schema
2use agent_client_protocol as acp;
3use agentic_coding_protocol::{self as acp_old, AgentRequest as _};
4use anyhow::{Context as _, Result};
5use futures::channel::oneshot;
6use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity};
7use project::Project;
8use std::{cell::RefCell, error::Error, fmt, path::Path, rc::Rc};
9use ui::App;
10
11use crate::{AcpThread, AcpThreadEvent, AgentConnection, ToolCallContent, ToolCallStatus};
12
13#[derive(Clone)]
14pub struct OldAcpClientDelegate {
15 thread: Rc<RefCell<WeakEntity<AcpThread>>>,
16 cx: AsyncApp,
17 next_tool_call_id: Rc<RefCell<u64>>,
18 // sent_buffer_versions: HashMap<Entity<Buffer>, HashMap<u64, BufferSnapshot>>,
19}
20
21impl OldAcpClientDelegate {
22 pub fn new(thread: Rc<RefCell<WeakEntity<AcpThread>>>, cx: AsyncApp) -> Self {
23 Self {
24 thread,
25 cx,
26 next_tool_call_id: Rc::new(RefCell::new(0)),
27 }
28 }
29}
30
31impl acp_old::Client for OldAcpClientDelegate {
32 async fn stream_assistant_message_chunk(
33 &self,
34 params: acp_old::StreamAssistantMessageChunkParams,
35 ) -> Result<(), acp_old::Error> {
36 let cx = &mut self.cx.clone();
37
38 cx.update(|cx| {
39 self.thread
40 .borrow()
41 .update(cx, |thread, cx| match params.chunk {
42 acp_old::AssistantMessageChunk::Text { text } => {
43 thread.push_assistant_chunk(text.into(), false, cx)
44 }
45 acp_old::AssistantMessageChunk::Thought { thought } => {
46 thread.push_assistant_chunk(thought.into(), true, cx)
47 }
48 })
49 .ok();
50 })?;
51
52 Ok(())
53 }
54
55 async fn request_tool_call_confirmation(
56 &self,
57 request: acp_old::RequestToolCallConfirmationParams,
58 ) -> Result<acp_old::RequestToolCallConfirmationResponse, acp_old::Error> {
59 let cx = &mut self.cx.clone();
60
61 let old_acp_id = *self.next_tool_call_id.borrow() + 1;
62 self.next_tool_call_id.replace(old_acp_id);
63
64 let tool_call = into_new_tool_call(
65 acp::ToolCallId(old_acp_id.to_string().into()),
66 request.tool_call,
67 );
68
69 let mut options = match request.confirmation {
70 acp_old::ToolCallConfirmation::Edit { .. } => vec![(
71 acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
72 acp::PermissionOptionKind::AllowAlways,
73 "Always Allow Edits".to_string(),
74 )],
75 acp_old::ToolCallConfirmation::Execute { root_command, .. } => vec![(
76 acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
77 acp::PermissionOptionKind::AllowAlways,
78 format!("Always Allow {}", root_command),
79 )],
80 acp_old::ToolCallConfirmation::Mcp {
81 server_name,
82 tool_name,
83 ..
84 } => vec![
85 (
86 acp_old::ToolCallConfirmationOutcome::AlwaysAllowMcpServer,
87 acp::PermissionOptionKind::AllowAlways,
88 format!("Always Allow {}", server_name),
89 ),
90 (
91 acp_old::ToolCallConfirmationOutcome::AlwaysAllowTool,
92 acp::PermissionOptionKind::AllowAlways,
93 format!("Always Allow {}", tool_name),
94 ),
95 ],
96 acp_old::ToolCallConfirmation::Fetch { .. } => vec![(
97 acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
98 acp::PermissionOptionKind::AllowAlways,
99 "Always Allow".to_string(),
100 )],
101 acp_old::ToolCallConfirmation::Other { .. } => vec![(
102 acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
103 acp::PermissionOptionKind::AllowAlways,
104 "Always Allow".to_string(),
105 )],
106 };
107
108 options.extend([
109 (
110 acp_old::ToolCallConfirmationOutcome::Allow,
111 acp::PermissionOptionKind::AllowOnce,
112 "Allow".to_string(),
113 ),
114 (
115 acp_old::ToolCallConfirmationOutcome::Reject,
116 acp::PermissionOptionKind::RejectOnce,
117 "Reject".to_string(),
118 ),
119 ]);
120
121 let mut outcomes = Vec::with_capacity(options.len());
122 let mut acp_options = Vec::with_capacity(options.len());
123
124 for (index, (outcome, kind, label)) in options.into_iter().enumerate() {
125 outcomes.push(outcome);
126 acp_options.push(acp::PermissionOption {
127 id: acp::PermissionOptionId(index.to_string().into()),
128 label,
129 kind,
130 })
131 }
132
133 let response = cx
134 .update(|cx| {
135 self.thread.borrow().update(cx, |thread, cx| {
136 thread.request_tool_call_permission(tool_call, acp_options, cx)
137 })
138 })?
139 .context("Failed to update thread")?
140 .await;
141
142 let outcome = match response {
143 Ok(option_id) => outcomes[option_id.0.parse::<usize>().unwrap_or(0)],
144 Err(oneshot::Canceled) => acp_old::ToolCallConfirmationOutcome::Cancel,
145 };
146
147 Ok(acp_old::RequestToolCallConfirmationResponse {
148 id: acp_old::ToolCallId(old_acp_id),
149 outcome: outcome,
150 })
151 }
152
153 async fn push_tool_call(
154 &self,
155 request: acp_old::PushToolCallParams,
156 ) -> Result<acp_old::PushToolCallResponse, acp_old::Error> {
157 let cx = &mut self.cx.clone();
158
159 let old_acp_id = *self.next_tool_call_id.borrow() + 1;
160 self.next_tool_call_id.replace(old_acp_id);
161
162 cx.update(|cx| {
163 self.thread.borrow().update(cx, |thread, cx| {
164 thread.upsert_tool_call(
165 into_new_tool_call(acp::ToolCallId(old_acp_id.to_string().into()), request),
166 cx,
167 )
168 })
169 })?
170 .context("Failed to update thread")?;
171
172 Ok(acp_old::PushToolCallResponse {
173 id: acp_old::ToolCallId(old_acp_id),
174 })
175 }
176
177 async fn update_tool_call(
178 &self,
179 request: acp_old::UpdateToolCallParams,
180 ) -> Result<(), acp_old::Error> {
181 let cx = &mut self.cx.clone();
182
183 cx.update(|cx| {
184 self.thread.borrow().update(cx, |thread, cx| {
185 let languages = thread.project.read(cx).languages().clone();
186
187 if let Some((ix, tool_call)) = thread
188 .tool_call_mut(&acp::ToolCallId(request.tool_call_id.0.to_string().into()))
189 {
190 tool_call.status = ToolCallStatus::Allowed {
191 status: into_new_tool_call_status(request.status),
192 };
193 tool_call.content = request
194 .content
195 .into_iter()
196 .map(|content| {
197 ToolCallContent::from_acp(
198 into_new_tool_call_content(content),
199 languages.clone(),
200 cx,
201 )
202 })
203 .collect();
204
205 cx.emit(AcpThreadEvent::EntryUpdated(ix));
206 anyhow::Ok(())
207 } else {
208 anyhow::bail!("Tool call not found")
209 }
210 })
211 })?
212 .context("Failed to update thread")??;
213
214 Ok(())
215 }
216
217 async fn update_plan(&self, request: acp_old::UpdatePlanParams) -> Result<(), acp_old::Error> {
218 let cx = &mut self.cx.clone();
219
220 cx.update(|cx| {
221 self.thread.borrow().update(cx, |thread, cx| {
222 thread.update_plan(
223 acp::Plan {
224 entries: request
225 .entries
226 .into_iter()
227 .map(into_new_plan_entry)
228 .collect(),
229 },
230 cx,
231 )
232 })
233 })?
234 .context("Failed to update thread")?;
235
236 Ok(())
237 }
238
239 async fn read_text_file(
240 &self,
241 acp_old::ReadTextFileParams { path, line, limit }: acp_old::ReadTextFileParams,
242 ) -> Result<acp_old::ReadTextFileResponse, acp_old::Error> {
243 let content = self
244 .cx
245 .update(|cx| {
246 self.thread.borrow().update(cx, |thread, cx| {
247 thread.read_text_file(path, line, limit, false, cx)
248 })
249 })?
250 .context("Failed to update thread")?
251 .await?;
252 Ok(acp_old::ReadTextFileResponse { content })
253 }
254
255 async fn write_text_file(
256 &self,
257 acp_old::WriteTextFileParams { path, content }: acp_old::WriteTextFileParams,
258 ) -> Result<(), acp_old::Error> {
259 self.cx
260 .update(|cx| {
261 self.thread
262 .borrow()
263 .update(cx, |thread, cx| thread.write_text_file(path, content, cx))
264 })?
265 .context("Failed to update thread")?
266 .await?;
267
268 Ok(())
269 }
270}
271
272fn into_new_tool_call(id: acp::ToolCallId, request: acp_old::PushToolCallParams) -> acp::ToolCall {
273 acp::ToolCall {
274 id: id,
275 label: request.label,
276 kind: acp_kind_from_old_icon(request.icon),
277 status: acp::ToolCallStatus::InProgress,
278 content: request
279 .content
280 .into_iter()
281 .map(into_new_tool_call_content)
282 .collect(),
283 locations: request
284 .locations
285 .into_iter()
286 .map(into_new_tool_call_location)
287 .collect(),
288 }
289}
290
291fn acp_kind_from_old_icon(icon: acp_old::Icon) -> acp::ToolKind {
292 match icon {
293 acp_old::Icon::FileSearch => acp::ToolKind::Search,
294 acp_old::Icon::Folder => acp::ToolKind::Search,
295 acp_old::Icon::Globe => acp::ToolKind::Search,
296 acp_old::Icon::Hammer => acp::ToolKind::Other,
297 acp_old::Icon::LightBulb => acp::ToolKind::Think,
298 acp_old::Icon::Pencil => acp::ToolKind::Edit,
299 acp_old::Icon::Regex => acp::ToolKind::Search,
300 acp_old::Icon::Terminal => acp::ToolKind::Execute,
301 }
302}
303
304fn into_new_tool_call_status(status: acp_old::ToolCallStatus) -> acp::ToolCallStatus {
305 match status {
306 acp_old::ToolCallStatus::Running => acp::ToolCallStatus::InProgress,
307 acp_old::ToolCallStatus::Finished => acp::ToolCallStatus::Completed,
308 acp_old::ToolCallStatus::Error => acp::ToolCallStatus::Failed,
309 }
310}
311
312fn into_new_tool_call_content(content: acp_old::ToolCallContent) -> acp::ToolCallContent {
313 match content {
314 acp_old::ToolCallContent::Markdown { markdown } => acp::ToolCallContent::ContentBlock {
315 content: acp::ContentBlock::Text(acp::TextContent {
316 annotations: None,
317 text: markdown,
318 }),
319 },
320 acp_old::ToolCallContent::Diff { diff } => acp::ToolCallContent::Diff {
321 diff: into_new_diff(diff),
322 },
323 }
324}
325
326fn into_new_diff(diff: acp_old::Diff) -> acp::Diff {
327 acp::Diff {
328 path: diff.path,
329 old_text: diff.old_text,
330 new_text: diff.new_text,
331 }
332}
333
334fn into_new_tool_call_location(location: acp_old::ToolCallLocation) -> acp::ToolCallLocation {
335 acp::ToolCallLocation {
336 path: location.path,
337 line: location.line,
338 }
339}
340
341fn into_new_plan_entry(entry: acp_old::PlanEntry) -> acp::PlanEntry {
342 acp::PlanEntry {
343 content: entry.content,
344 priority: into_new_plan_priority(entry.priority),
345 status: into_new_plan_status(entry.status),
346 }
347}
348
349fn into_new_plan_priority(priority: acp_old::PlanEntryPriority) -> acp::PlanEntryPriority {
350 match priority {
351 acp_old::PlanEntryPriority::Low => acp::PlanEntryPriority::Low,
352 acp_old::PlanEntryPriority::Medium => acp::PlanEntryPriority::Medium,
353 acp_old::PlanEntryPriority::High => acp::PlanEntryPriority::High,
354 }
355}
356
357fn into_new_plan_status(status: acp_old::PlanEntryStatus) -> acp::PlanEntryStatus {
358 match status {
359 acp_old::PlanEntryStatus::Pending => acp::PlanEntryStatus::Pending,
360 acp_old::PlanEntryStatus::InProgress => acp::PlanEntryStatus::InProgress,
361 acp_old::PlanEntryStatus::Completed => acp::PlanEntryStatus::Completed,
362 }
363}
364
365#[derive(Debug)]
366pub struct Unauthenticated;
367
368impl Error for Unauthenticated {}
369impl fmt::Display for Unauthenticated {
370 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
371 write!(f, "Unauthenticated")
372 }
373}
374
375pub struct OldAcpAgentConnection {
376 pub name: &'static str,
377 pub connection: acp_old::AgentConnection,
378 pub child_status: Task<Result<()>>,
379}
380
381impl AgentConnection for OldAcpAgentConnection {
382 fn name(&self) -> &'static str {
383 self.name
384 }
385
386 fn new_thread(
387 self: Rc<Self>,
388 project: Entity<Project>,
389 _cwd: &Path,
390 cx: &mut AsyncApp,
391 ) -> Task<Result<Entity<AcpThread>>> {
392 let task = self.connection.request_any(
393 acp_old::InitializeParams {
394 protocol_version: acp_old::ProtocolVersion::latest(),
395 }
396 .into_any(),
397 );
398 cx.spawn(async move |cx| {
399 let result = task.await?;
400 let result = acp_old::InitializeParams::response_from_any(result)?;
401
402 if !result.is_authenticated {
403 anyhow::bail!(Unauthenticated)
404 }
405
406 cx.update(|cx| {
407 let thread = cx.new(|cx| {
408 let session_id = acp::SessionId("acp-old-no-id".into());
409 AcpThread::new(self.clone(), project, session_id, cx)
410 });
411 thread
412 })
413 })
414 }
415
416 fn authenticate(&self, cx: &mut App) -> Task<Result<()>> {
417 let task = self
418 .connection
419 .request_any(acp_old::AuthenticateParams.into_any());
420 cx.foreground_executor().spawn(async move {
421 task.await?;
422 Ok(())
423 })
424 }
425
426 fn prompt(&self, params: acp::PromptToolArguments, cx: &mut App) -> Task<Result<()>> {
427 let chunks = params
428 .prompt
429 .into_iter()
430 .filter_map(|block| match block {
431 acp::ContentBlock::Text(text) => {
432 Some(acp_old::UserMessageChunk::Text { text: text.text })
433 }
434 acp::ContentBlock::ResourceLink(link) => Some(acp_old::UserMessageChunk::Path {
435 path: link.uri.into(),
436 }),
437 _ => None,
438 })
439 .collect();
440
441 let task = self
442 .connection
443 .request_any(acp_old::SendUserMessageParams { chunks }.into_any());
444 cx.foreground_executor().spawn(async move {
445 task.await?;
446 anyhow::Ok(())
447 })
448 }
449
450 fn cancel(&self, _session_id: &acp::SessionId, cx: &mut App) {
451 let task = self
452 .connection
453 .request_any(acp_old::CancelSendMessageParams.into_any());
454 cx.foreground_executor()
455 .spawn(async move {
456 task.await?;
457 anyhow::Ok(())
458 })
459 .detach_and_log_err(cx)
460 }
461}