// Translates old acp agents into the new schema use action_log::ActionLog; use agent_client_protocol as acp; use agentic_coding_protocol::{self as acp_old, AgentRequest as _}; use anyhow::{Context as _, Result, anyhow}; use futures::channel::oneshot; use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity}; use project::Project; use std::{any::Any, cell::RefCell, path::Path, rc::Rc}; use ui::App; use util::ResultExt as _; use crate::AgentServerCommand; use acp_thread::{AcpThread, AgentConnection, AuthRequired}; #[derive(Clone)] struct OldAcpClientDelegate { thread: Rc>>, cx: AsyncApp, next_tool_call_id: Rc>, // sent_buffer_versions: HashMap, HashMap>, } impl OldAcpClientDelegate { fn new(thread: Rc>>, cx: AsyncApp) -> Self { Self { thread, cx, next_tool_call_id: Rc::new(RefCell::new(0)), } } } impl acp_old::Client for OldAcpClientDelegate { async fn stream_assistant_message_chunk( &self, params: acp_old::StreamAssistantMessageChunkParams, ) -> Result<(), acp_old::Error> { let cx = &mut self.cx.clone(); cx.update(|cx| { self.thread .borrow() .update(cx, |thread, cx| match params.chunk { acp_old::AssistantMessageChunk::Text { text } => { thread.push_assistant_content_block(text.into(), false, cx) } acp_old::AssistantMessageChunk::Thought { thought } => { thread.push_assistant_content_block(thought.into(), true, cx) } }) .log_err(); })?; Ok(()) } async fn request_tool_call_confirmation( &self, request: acp_old::RequestToolCallConfirmationParams, ) -> Result { let cx = &mut self.cx.clone(); let old_acp_id = *self.next_tool_call_id.borrow() + 1; self.next_tool_call_id.replace(old_acp_id); let tool_call = into_new_tool_call( acp::ToolCallId(old_acp_id.to_string().into()), request.tool_call, ); let mut options = match request.confirmation { acp_old::ToolCallConfirmation::Edit { .. } => vec![( acp_old::ToolCallConfirmationOutcome::AlwaysAllow, acp::PermissionOptionKind::AllowAlways, "Always Allow Edits".to_string(), )], acp_old::ToolCallConfirmation::Execute { root_command, .. } => vec![( acp_old::ToolCallConfirmationOutcome::AlwaysAllow, acp::PermissionOptionKind::AllowAlways, format!("Always Allow {}", root_command), )], acp_old::ToolCallConfirmation::Mcp { server_name, tool_name, .. } => vec![ ( acp_old::ToolCallConfirmationOutcome::AlwaysAllowMcpServer, acp::PermissionOptionKind::AllowAlways, format!("Always Allow {}", server_name), ), ( acp_old::ToolCallConfirmationOutcome::AlwaysAllowTool, acp::PermissionOptionKind::AllowAlways, format!("Always Allow {}", tool_name), ), ], acp_old::ToolCallConfirmation::Fetch { .. } => vec![( acp_old::ToolCallConfirmationOutcome::AlwaysAllow, acp::PermissionOptionKind::AllowAlways, "Always Allow".to_string(), )], acp_old::ToolCallConfirmation::Other { .. } => vec![( acp_old::ToolCallConfirmationOutcome::AlwaysAllow, acp::PermissionOptionKind::AllowAlways, "Always Allow".to_string(), )], }; options.extend([ ( acp_old::ToolCallConfirmationOutcome::Allow, acp::PermissionOptionKind::AllowOnce, "Allow".to_string(), ), ( acp_old::ToolCallConfirmationOutcome::Reject, acp::PermissionOptionKind::RejectOnce, "Reject".to_string(), ), ]); let mut outcomes = Vec::with_capacity(options.len()); let mut acp_options = Vec::with_capacity(options.len()); for (index, (outcome, kind, label)) in options.into_iter().enumerate() { outcomes.push(outcome); acp_options.push(acp::PermissionOption { id: acp::PermissionOptionId(index.to_string().into()), name: label, kind, }) } let response = cx .update(|cx| { self.thread.borrow().update(cx, |thread, cx| { thread.request_tool_call_authorization(tool_call.into(), acp_options, cx) }) })?? .context("Failed to update thread")? .await; let outcome = match response { Ok(option_id) => outcomes[option_id.0.parse::().unwrap_or(0)], Err(oneshot::Canceled) => acp_old::ToolCallConfirmationOutcome::Cancel, }; Ok(acp_old::RequestToolCallConfirmationResponse { id: acp_old::ToolCallId(old_acp_id), outcome, }) } async fn push_tool_call( &self, request: acp_old::PushToolCallParams, ) -> Result { let cx = &mut self.cx.clone(); let old_acp_id = *self.next_tool_call_id.borrow() + 1; self.next_tool_call_id.replace(old_acp_id); cx.update(|cx| { self.thread.borrow().update(cx, |thread, cx| { thread.upsert_tool_call( into_new_tool_call(acp::ToolCallId(old_acp_id.to_string().into()), request), cx, ) }) })?? .context("Failed to update thread")?; Ok(acp_old::PushToolCallResponse { id: acp_old::ToolCallId(old_acp_id), }) } async fn update_tool_call( &self, request: acp_old::UpdateToolCallParams, ) -> Result<(), acp_old::Error> { let cx = &mut self.cx.clone(); cx.update(|cx| { self.thread.borrow().update(cx, |thread, cx| { thread.update_tool_call( acp::ToolCallUpdate { id: acp::ToolCallId(request.tool_call_id.0.to_string().into()), fields: acp::ToolCallUpdateFields { status: Some(into_new_tool_call_status(request.status)), content: Some( request .content .into_iter() .map(into_new_tool_call_content) .collect::>(), ), ..Default::default() }, }, cx, ) }) })? .context("Failed to update thread")??; Ok(()) } async fn update_plan(&self, request: acp_old::UpdatePlanParams) -> Result<(), acp_old::Error> { let cx = &mut self.cx.clone(); cx.update(|cx| { self.thread.borrow().update(cx, |thread, cx| { thread.update_plan( acp::Plan { entries: request .entries .into_iter() .map(into_new_plan_entry) .collect(), }, cx, ) }) })? .context("Failed to update thread")?; Ok(()) } async fn read_text_file( &self, acp_old::ReadTextFileParams { path, line, limit }: acp_old::ReadTextFileParams, ) -> Result { let content = self .cx .update(|cx| { self.thread.borrow().update(cx, |thread, cx| { thread.read_text_file(path, line, limit, false, cx) }) })? .context("Failed to update thread")? .await?; Ok(acp_old::ReadTextFileResponse { content }) } async fn write_text_file( &self, acp_old::WriteTextFileParams { path, content }: acp_old::WriteTextFileParams, ) -> Result<(), acp_old::Error> { self.cx .update(|cx| { self.thread .borrow() .update(cx, |thread, cx| thread.write_text_file(path, content, cx)) })? .context("Failed to update thread")? .await?; Ok(()) } } fn into_new_tool_call(id: acp::ToolCallId, request: acp_old::PushToolCallParams) -> acp::ToolCall { acp::ToolCall { id, title: request.label, kind: acp_kind_from_old_icon(request.icon), status: acp::ToolCallStatus::InProgress, content: request .content .into_iter() .map(into_new_tool_call_content) .collect(), locations: request .locations .into_iter() .map(into_new_tool_call_location) .collect(), raw_input: None, raw_output: None, } } fn acp_kind_from_old_icon(icon: acp_old::Icon) -> acp::ToolKind { match icon { acp_old::Icon::FileSearch => acp::ToolKind::Search, acp_old::Icon::Folder => acp::ToolKind::Search, acp_old::Icon::Globe => acp::ToolKind::Search, acp_old::Icon::Hammer => acp::ToolKind::Other, acp_old::Icon::LightBulb => acp::ToolKind::Think, acp_old::Icon::Pencil => acp::ToolKind::Edit, acp_old::Icon::Regex => acp::ToolKind::Search, acp_old::Icon::Terminal => acp::ToolKind::Execute, } } fn into_new_tool_call_status(status: acp_old::ToolCallStatus) -> acp::ToolCallStatus { match status { acp_old::ToolCallStatus::Running => acp::ToolCallStatus::InProgress, acp_old::ToolCallStatus::Finished => acp::ToolCallStatus::Completed, acp_old::ToolCallStatus::Error => acp::ToolCallStatus::Failed, } } fn into_new_tool_call_content(content: acp_old::ToolCallContent) -> acp::ToolCallContent { match content { acp_old::ToolCallContent::Markdown { markdown } => markdown.into(), acp_old::ToolCallContent::Diff { diff } => acp::ToolCallContent::Diff { diff: into_new_diff(diff), }, } } fn into_new_diff(diff: acp_old::Diff) -> acp::Diff { acp::Diff { path: diff.path, old_text: diff.old_text, new_text: diff.new_text, } } fn into_new_tool_call_location(location: acp_old::ToolCallLocation) -> acp::ToolCallLocation { acp::ToolCallLocation { path: location.path, line: location.line, } } fn into_new_plan_entry(entry: acp_old::PlanEntry) -> acp::PlanEntry { acp::PlanEntry { content: entry.content, priority: into_new_plan_priority(entry.priority), status: into_new_plan_status(entry.status), } } fn into_new_plan_priority(priority: acp_old::PlanEntryPriority) -> acp::PlanEntryPriority { match priority { acp_old::PlanEntryPriority::Low => acp::PlanEntryPriority::Low, acp_old::PlanEntryPriority::Medium => acp::PlanEntryPriority::Medium, acp_old::PlanEntryPriority::High => acp::PlanEntryPriority::High, } } fn into_new_plan_status(status: acp_old::PlanEntryStatus) -> acp::PlanEntryStatus { match status { acp_old::PlanEntryStatus::Pending => acp::PlanEntryStatus::Pending, acp_old::PlanEntryStatus::InProgress => acp::PlanEntryStatus::InProgress, acp_old::PlanEntryStatus::Completed => acp::PlanEntryStatus::Completed, } } pub struct AcpConnection { pub name: &'static str, pub connection: acp_old::AgentConnection, pub _child_status: Task>, pub current_thread: Rc>>, } impl AcpConnection { pub fn stdio( name: &'static str, command: AgentServerCommand, root_dir: &Path, cx: &mut AsyncApp, ) -> Task> { let root_dir = root_dir.to_path_buf(); cx.spawn(async move |cx| { let mut child = util::command::new_smol_command(&command.path) .args(command.args.iter()) .current_dir(root_dir) .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::inherit()) .kill_on_drop(true) .spawn()?; let stdin = child.stdin.take().unwrap(); let stdout = child.stdout.take().unwrap(); log::trace!("Spawned (pid: {})", child.id()); let foreground_executor = cx.foreground_executor().clone(); let thread_rc = Rc::new(RefCell::new(WeakEntity::new_invalid())); let (connection, io_fut) = acp_old::AgentConnection::connect_to_agent( OldAcpClientDelegate::new(thread_rc.clone(), cx.clone()), stdin, stdout, move |fut| foreground_executor.spawn(fut).detach(), ); let io_task = cx.background_spawn(async move { io_fut.await.log_err(); }); let child_status = cx.background_spawn(async move { let result = match child.status().await { Err(e) => Err(anyhow!(e)), Ok(result) if result.success() => Ok(()), Ok(result) => Err(anyhow!(result)), }; drop(io_task); result }); Ok(Self { name, connection, _child_status: child_status, current_thread: thread_rc, }) }) } } impl AgentConnection for AcpConnection { fn new_thread( self: Rc, project: Entity, _cwd: &Path, cx: &mut App, ) -> Task>> { let task = self.connection.request_any( acp_old::InitializeParams { protocol_version: acp_old::ProtocolVersion::latest(), } .into_any(), ); let current_thread = self.current_thread.clone(); cx.spawn(async move |cx| { let result = task.await?; let result = acp_old::InitializeParams::response_from_any(result)?; if !result.is_authenticated { anyhow::bail!(AuthRequired::new()) } cx.update(|cx| { let thread = cx.new(|cx| { let session_id = acp::SessionId("acp-old-no-id".into()); let action_log = cx.new(|_| ActionLog::new(project.clone())); AcpThread::new(self.name, self.clone(), project, action_log, session_id) }); current_thread.replace(thread.downgrade()); thread }) }) } fn auth_methods(&self) -> &[acp::AuthMethod] { &[] } fn authenticate(&self, _method_id: acp::AuthMethodId, cx: &mut App) -> Task> { let task = self .connection .request_any(acp_old::AuthenticateParams.into_any()); cx.foreground_executor().spawn(async move { task.await?; Ok(()) }) } fn prompt( &self, _id: Option, params: acp::PromptRequest, cx: &mut App, ) -> Task> { let chunks = params .prompt .into_iter() .filter_map(|block| match block { acp::ContentBlock::Text(text) => { Some(acp_old::UserMessageChunk::Text { text: text.text }) } acp::ContentBlock::ResourceLink(link) => Some(acp_old::UserMessageChunk::Path { path: link.uri.into(), }), _ => None, }) .collect(); let task = self .connection .request_any(acp_old::SendUserMessageParams { chunks }.into_any()); cx.foreground_executor().spawn(async move { task.await?; anyhow::Ok(acp::PromptResponse { stop_reason: acp::StopReason::EndTurn, }) }) } fn prompt_capabilities(&self) -> acp::PromptCapabilities { acp::PromptCapabilities { image: false, audio: false, embedded_context: false, } } fn cancel(&self, _session_id: &acp::SessionId, cx: &mut App) { let task = self .connection .request_any(acp_old::CancelSendMessageParams.into_any()); cx.foreground_executor() .spawn(async move { task.await?; anyhow::Ok(()) }) .detach_and_log_err(cx) } fn into_any(self: Rc) -> Rc { self } }