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