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, 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}