1pub mod permissions;
2
3use std::cell::RefCell;
4use std::collections::HashMap;
5
6use agent_client_protocol as acp;
7use tokio::sync::{mpsc, oneshot};
8
9use crate::bridge::events::{
10 BridgeEvent, PermissionKind, PermissionOption, PermissionOutcome, ToolCallInfo,
11};
12
13pub struct BridgedAcpClient {
20 pub evt_tx: mpsc::UnboundedSender<BridgeEvent>,
21 tool_call_state: RefCell<HashMap<String, (String, bool)>>,
24}
25
26impl BridgedAcpClient {
27 pub fn new(evt_tx: mpsc::UnboundedSender<BridgeEvent>) -> Self {
28 Self {
29 evt_tx,
30 tool_call_state: RefCell::new(HashMap::new()),
31 }
32 }
33}
34
35#[async_trait::async_trait(?Send)]
36impl acp::Client for BridgedAcpClient {
37 async fn request_permission(
38 &self,
39 args: acp::RequestPermissionRequest,
40 ) -> acp::Result<acp::RequestPermissionResponse> {
41 let tool = ToolCallInfo {
42 name: args.tool_call.fields.title.clone().unwrap_or_default(),
43 description: None,
44 };
45
46 let options: Vec<PermissionOption> = args
47 .options
48 .iter()
49 .map(|o| PermissionOption {
50 option_id: o.option_id.0.to_string(),
51 name: o.name.clone(),
52 kind: match o.kind {
53 acp::PermissionOptionKind::AllowOnce
54 | acp::PermissionOptionKind::AllowAlways => PermissionKind::Allow,
55 _ => PermissionKind::Deny,
56 },
57 })
58 .collect();
59
60 let (reply_tx, reply_rx) = oneshot::channel();
61 let _ = self.evt_tx.send(BridgeEvent::PermissionRequest {
62 tool,
63 options,
64 reply: reply_tx,
65 });
66
67 let outcome = reply_rx.await.unwrap_or(PermissionOutcome::Cancelled);
68
69 let acp_outcome = match outcome {
70 PermissionOutcome::Selected { option_id } => acp::RequestPermissionOutcome::Selected(
71 acp::SelectedPermissionOutcome::new(option_id),
72 ),
73 PermissionOutcome::Cancelled => acp::RequestPermissionOutcome::Cancelled,
74 };
75
76 Ok(acp::RequestPermissionResponse::new(acp_outcome))
77 }
78
79 async fn session_notification(&self, args: acp::SessionNotification) -> acp::Result<()> {
80 match args.update {
81 acp::SessionUpdate::AgentMessageChunk(chunk) => {
82 if let acp::ContentBlock::Text(text_content) = chunk.content {
83 let _ = self.evt_tx.send(BridgeEvent::TextChunk {
84 text: text_content.text,
85 });
86 }
87 }
88 acp::SessionUpdate::ToolCall(tool_call) => {
89 let is_read = matches!(tool_call.kind, acp::ToolKind::Read);
92 let id = tool_call.tool_call_id.0.to_string();
93 let title = tool_call.title.clone();
94 self.tool_call_state
95 .borrow_mut()
96 .insert(id, (title.clone(), is_read));
97 let _ = self.evt_tx.send(BridgeEvent::ToolUse { name: title });
98 }
99 acp::SessionUpdate::ToolCallUpdate(update) => {
100 let done = matches!(
102 update.fields.status,
103 Some(acp::ToolCallStatus::Completed) | Some(acp::ToolCallStatus::Failed)
104 );
105 if done {
106 let id = update.tool_call_id.0.to_string();
107 let (title, is_read) = self
108 .tool_call_state
109 .borrow_mut()
110 .remove(&id)
111 .unwrap_or_else(|| {
112 eprintln!("[acp-cli] warning: ToolCallUpdate for unknown id={id}");
115 (String::new(), false)
116 });
117 let output = update
118 .fields
119 .content
120 .as_deref()
121 .map(extract_text_output)
122 .unwrap_or_default();
123 let _ = self.evt_tx.send(BridgeEvent::ToolResult {
124 name: title,
125 output,
126 is_read,
127 });
128 }
129 }
130 _ => {
131 }
133 }
134 Ok(())
135 }
136}
137
138fn extract_text_output(content: &[acp::ToolCallContent]) -> String {
145 content
146 .iter()
147 .filter_map(|c| {
148 if let acp::ToolCallContent::Content(block) = c
149 && let acp::ContentBlock::Text(text) = &block.content
150 {
151 return Some(text.text.clone());
152 }
153 None
154 })
155 .collect::<Vec<_>>()
156 .join("")
157}