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, AgentConnection};
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_content_block(text.into(), false, cx)
44 }
45 acp_old::AssistantMessageChunk::Thought { thought } => {
46 thread.push_assistant_content_block(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 thread.update_tool_call(
186 acp::ToolCallUpdate {
187 id: acp::ToolCallId(request.tool_call_id.0.to_string().into()),
188 fields: acp::ToolCallUpdateFields {
189 status: Some(into_new_tool_call_status(request.status)),
190 content: Some(
191 request
192 .content
193 .into_iter()
194 .map(into_new_tool_call_content)
195 .collect::<Vec<_>>(),
196 ),
197 ..Default::default()
198 },
199 },
200 cx,
201 )
202 })
203 })?
204 .context("Failed to update thread")??;
205
206 Ok(())
207 }
208
209 async fn update_plan(&self, request: acp_old::UpdatePlanParams) -> Result<(), acp_old::Error> {
210 let cx = &mut self.cx.clone();
211
212 cx.update(|cx| {
213 self.thread.borrow().update(cx, |thread, cx| {
214 thread.update_plan(
215 acp::Plan {
216 entries: request
217 .entries
218 .into_iter()
219 .map(into_new_plan_entry)
220 .collect(),
221 },
222 cx,
223 )
224 })
225 })?
226 .context("Failed to update thread")?;
227
228 Ok(())
229 }
230
231 async fn read_text_file(
232 &self,
233 acp_old::ReadTextFileParams { path, line, limit }: acp_old::ReadTextFileParams,
234 ) -> Result<acp_old::ReadTextFileResponse, acp_old::Error> {
235 let content = self
236 .cx
237 .update(|cx| {
238 self.thread.borrow().update(cx, |thread, cx| {
239 thread.read_text_file(path, line, limit, false, cx)
240 })
241 })?
242 .context("Failed to update thread")?
243 .await?;
244 Ok(acp_old::ReadTextFileResponse { content })
245 }
246
247 async fn write_text_file(
248 &self,
249 acp_old::WriteTextFileParams { path, content }: acp_old::WriteTextFileParams,
250 ) -> Result<(), acp_old::Error> {
251 self.cx
252 .update(|cx| {
253 self.thread
254 .borrow()
255 .update(cx, |thread, cx| thread.write_text_file(path, content, cx))
256 })?
257 .context("Failed to update thread")?
258 .await?;
259
260 Ok(())
261 }
262}
263
264fn into_new_tool_call(id: acp::ToolCallId, request: acp_old::PushToolCallParams) -> acp::ToolCall {
265 acp::ToolCall {
266 id: id,
267 label: request.label,
268 kind: acp_kind_from_old_icon(request.icon),
269 status: acp::ToolCallStatus::InProgress,
270 content: request
271 .content
272 .into_iter()
273 .map(into_new_tool_call_content)
274 .collect(),
275 locations: request
276 .locations
277 .into_iter()
278 .map(into_new_tool_call_location)
279 .collect(),
280 raw_input: None,
281 }
282}
283
284fn acp_kind_from_old_icon(icon: acp_old::Icon) -> acp::ToolKind {
285 match icon {
286 acp_old::Icon::FileSearch => acp::ToolKind::Search,
287 acp_old::Icon::Folder => acp::ToolKind::Search,
288 acp_old::Icon::Globe => acp::ToolKind::Search,
289 acp_old::Icon::Hammer => acp::ToolKind::Other,
290 acp_old::Icon::LightBulb => acp::ToolKind::Think,
291 acp_old::Icon::Pencil => acp::ToolKind::Edit,
292 acp_old::Icon::Regex => acp::ToolKind::Search,
293 acp_old::Icon::Terminal => acp::ToolKind::Execute,
294 }
295}
296
297fn into_new_tool_call_status(status: acp_old::ToolCallStatus) -> acp::ToolCallStatus {
298 match status {
299 acp_old::ToolCallStatus::Running => acp::ToolCallStatus::InProgress,
300 acp_old::ToolCallStatus::Finished => acp::ToolCallStatus::Completed,
301 acp_old::ToolCallStatus::Error => acp::ToolCallStatus::Failed,
302 }
303}
304
305fn into_new_tool_call_content(content: acp_old::ToolCallContent) -> acp::ToolCallContent {
306 match content {
307 acp_old::ToolCallContent::Markdown { markdown } => markdown.into(),
308 acp_old::ToolCallContent::Diff { diff } => acp::ToolCallContent::Diff {
309 diff: into_new_diff(diff),
310 },
311 }
312}
313
314fn into_new_diff(diff: acp_old::Diff) -> acp::Diff {
315 acp::Diff {
316 path: diff.path,
317 old_text: diff.old_text,
318 new_text: diff.new_text,
319 }
320}
321
322fn into_new_tool_call_location(location: acp_old::ToolCallLocation) -> acp::ToolCallLocation {
323 acp::ToolCallLocation {
324 path: location.path,
325 line: location.line,
326 }
327}
328
329fn into_new_plan_entry(entry: acp_old::PlanEntry) -> acp::PlanEntry {
330 acp::PlanEntry {
331 content: entry.content,
332 priority: into_new_plan_priority(entry.priority),
333 status: into_new_plan_status(entry.status),
334 }
335}
336
337fn into_new_plan_priority(priority: acp_old::PlanEntryPriority) -> acp::PlanEntryPriority {
338 match priority {
339 acp_old::PlanEntryPriority::Low => acp::PlanEntryPriority::Low,
340 acp_old::PlanEntryPriority::Medium => acp::PlanEntryPriority::Medium,
341 acp_old::PlanEntryPriority::High => acp::PlanEntryPriority::High,
342 }
343}
344
345fn into_new_plan_status(status: acp_old::PlanEntryStatus) -> acp::PlanEntryStatus {
346 match status {
347 acp_old::PlanEntryStatus::Pending => acp::PlanEntryStatus::Pending,
348 acp_old::PlanEntryStatus::InProgress => acp::PlanEntryStatus::InProgress,
349 acp_old::PlanEntryStatus::Completed => acp::PlanEntryStatus::Completed,
350 }
351}
352
353#[derive(Debug)]
354pub struct Unauthenticated;
355
356impl Error for Unauthenticated {}
357impl fmt::Display for Unauthenticated {
358 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
359 write!(f, "Unauthenticated")
360 }
361}
362
363pub struct OldAcpAgentConnection {
364 pub name: &'static str,
365 pub connection: acp_old::AgentConnection,
366 pub child_status: Task<Result<()>>,
367}
368
369impl AgentConnection for OldAcpAgentConnection {
370 fn name(&self) -> &'static str {
371 self.name
372 }
373
374 fn new_thread(
375 self: Rc<Self>,
376 project: Entity<Project>,
377 _cwd: &Path,
378 cx: &mut AsyncApp,
379 ) -> Task<Result<Entity<AcpThread>>> {
380 let task = self.connection.request_any(
381 acp_old::InitializeParams {
382 protocol_version: acp_old::ProtocolVersion::latest(),
383 }
384 .into_any(),
385 );
386 cx.spawn(async move |cx| {
387 let result = task.await?;
388 let result = acp_old::InitializeParams::response_from_any(result)?;
389
390 if !result.is_authenticated {
391 anyhow::bail!(Unauthenticated)
392 }
393
394 cx.update(|cx| {
395 let thread = cx.new(|cx| {
396 let session_id = acp::SessionId("acp-old-no-id".into());
397 AcpThread::new(self.clone(), project, session_id, cx)
398 });
399 thread
400 })
401 })
402 }
403
404 fn authenticate(&self, cx: &mut App) -> Task<Result<()>> {
405 let task = self
406 .connection
407 .request_any(acp_old::AuthenticateParams.into_any());
408 cx.foreground_executor().spawn(async move {
409 task.await?;
410 Ok(())
411 })
412 }
413
414 fn prompt(&self, params: acp::PromptArguments, cx: &mut App) -> Task<Result<()>> {
415 let chunks = params
416 .prompt
417 .into_iter()
418 .filter_map(|block| match block {
419 acp::ContentBlock::Text(text) => {
420 Some(acp_old::UserMessageChunk::Text { text: text.text })
421 }
422 acp::ContentBlock::ResourceLink(link) => Some(acp_old::UserMessageChunk::Path {
423 path: link.uri.into(),
424 }),
425 _ => None,
426 })
427 .collect();
428
429 let task = self
430 .connection
431 .request_any(acp_old::SendUserMessageParams { chunks }.into_any());
432 cx.foreground_executor().spawn(async move {
433 task.await?;
434 anyhow::Ok(())
435 })
436 }
437
438 fn cancel(&self, _session_id: &acp::SessionId, cx: &mut App) {
439 let task = self
440 .connection
441 .request_any(acp_old::CancelSendMessageParams.into_any());
442 cx.foreground_executor()
443 .spawn(async move {
444 task.await?;
445 anyhow::Ok(())
446 })
447 .detach_and_log_err(cx)
448 }
449}