v0.rs

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