old_acp_support.rs

  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}