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