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