claude_code_acp/terminal/
client.rs1use std::path::PathBuf;
6use std::sync::Arc;
7use std::time::Instant;
8
9use sacp::JrConnectionCx;
10use sacp::link::AgentToClient;
11use sacp::schema::{
12 CreateTerminalRequest, CreateTerminalResponse, EnvVariable, KillTerminalCommandRequest,
13 KillTerminalCommandResponse, ReleaseTerminalRequest, ReleaseTerminalResponse, SessionId,
14 TerminalId, TerminalOutputRequest, TerminalOutputResponse, WaitForTerminalExitRequest,
15 WaitForTerminalExitResponse,
16};
17use tracing::instrument;
18
19use crate::types::AgentError;
20
21#[derive(Debug, Clone)]
27pub struct TerminalClient {
28 connection_cx: JrConnectionCx<AgentToClient>,
30 session_id: SessionId,
32}
33
34impl TerminalClient {
35 pub fn new(
37 connection_cx: JrConnectionCx<AgentToClient>,
38 session_id: impl Into<SessionId>,
39 ) -> Self {
40 Self {
41 connection_cx,
42 session_id: session_id.into(),
43 }
44 }
45
46 #[instrument(
58 name = "terminal_create",
59 skip(self, command, args, cwd),
60 fields(
61 session_id = %self.session_id.0,
62 args_count = args.len(),
63 has_cwd = cwd.is_some(),
64 )
65 )]
66 pub async fn create(
67 &self,
68 command: impl Into<String>,
69 args: Vec<String>,
70 cwd: Option<PathBuf>,
71 output_byte_limit: Option<u64>,
72 ) -> Result<TerminalId, AgentError> {
73 let start_time = Instant::now();
74 let cmd: String = command.into();
75
76 tracing::info!(
77 command = %cmd,
78 args = ?args,
79 cwd = ?cwd,
80 output_byte_limit = ?output_byte_limit,
81 "Creating terminal and executing command"
82 );
83
84 let mut request = CreateTerminalRequest::new(self.session_id.clone(), cmd.clone());
85 request = request.args(args.clone());
86
87 request = request.env(vec![EnvVariable::new("CLAUDECODE", "1")]);
89
90 if let Some(cwd_path) = cwd.clone() {
91 request = request.cwd(cwd_path);
92 }
93
94 if let Some(limit) = output_byte_limit {
95 request = request.output_byte_limit(limit);
96 }
97
98 tracing::debug!("Sending terminal/create request to ACP client");
99
100 let response: CreateTerminalResponse = self
101 .connection_cx
102 .send_request(request)
103 .block_task()
104 .await
105 .map_err(|e| {
106 let elapsed = start_time.elapsed();
107 tracing::error!(
108 session_id = %self.session_id.0,
109 command = %cmd,
110 error = %e,
111 error_type = %std::any::type_name::<sacp::Error>(),
112 elapsed_ms = elapsed.as_millis(),
113 "Terminal create request failed"
114 );
115 AgentError::Internal(format!("Terminal create failed: {}", e))
116 })?;
117
118 let elapsed = start_time.elapsed();
119 tracing::info!(
120 terminal_id = %response.terminal_id.0,
121 command = %cmd,
122 elapsed_ms = elapsed.as_millis(),
123 "Terminal created successfully"
124 );
125
126 Ok(response.terminal_id)
127 }
128
129 #[instrument(
133 name = "terminal_output",
134 skip(self, terminal_id),
135 fields(session_id = %self.session_id.0)
136 )]
137 pub async fn output(
138 &self,
139 terminal_id: impl Into<TerminalId>,
140 ) -> Result<TerminalOutputResponse, AgentError> {
141 let start_time = Instant::now();
142 let tid: TerminalId = terminal_id.into();
143
144 tracing::debug!(
145 terminal_id = %tid.0,
146 "Getting terminal output"
147 );
148
149 let request = TerminalOutputRequest::new(self.session_id.clone(), tid.clone());
150
151 let response = self
152 .connection_cx
153 .send_request(request)
154 .block_task()
155 .await
156 .map_err(|e| {
157 let elapsed = start_time.elapsed();
158 tracing::error!(
159 terminal_id = %tid.0,
160 error = %e,
161 elapsed_ms = elapsed.as_millis(),
162 "Terminal output request failed"
163 );
164 AgentError::Internal(format!("Terminal output failed: {}", e))
165 })?;
166
167 let elapsed = start_time.elapsed();
168 tracing::debug!(
169 terminal_id = %tid.0,
170 elapsed_ms = elapsed.as_millis(),
171 output_len = response.output.len(),
172 exit_status = ?response.exit_status,
173 "Terminal output retrieved"
174 );
175
176 Ok(response)
177 }
178
179 #[instrument(
183 name = "terminal_wait_for_exit",
184 skip(self, terminal_id),
185 fields(session_id = %self.session_id.0)
186 )]
187 pub async fn wait_for_exit(
188 &self,
189 terminal_id: impl Into<TerminalId>,
190 ) -> Result<WaitForTerminalExitResponse, AgentError> {
191 let start_time = Instant::now();
192 let tid: TerminalId = terminal_id.into();
193
194 tracing::info!(
195 terminal_id = %tid.0,
196 "Waiting for terminal command to exit"
197 );
198
199 let request = WaitForTerminalExitRequest::new(self.session_id.clone(), tid.clone());
200
201 let response = self
202 .connection_cx
203 .send_request(request)
204 .block_task()
205 .await
206 .map_err(|e| {
207 let elapsed = start_time.elapsed();
208 tracing::error!(
209 terminal_id = %tid.0,
210 error = %e,
211 elapsed_ms = elapsed.as_millis(),
212 "Terminal wait_for_exit failed"
213 );
214 AgentError::Internal(format!("Terminal wait_for_exit failed: {}", e))
215 })?;
216
217 let elapsed = start_time.elapsed();
218 tracing::info!(
219 terminal_id = %tid.0,
220 elapsed_ms = elapsed.as_millis(),
221 exit_status = ?response.exit_status,
222 "Terminal command exited"
223 );
224
225 Ok(response)
226 }
227
228 #[instrument(
233 name = "terminal_kill",
234 skip(self, terminal_id),
235 fields(session_id = %self.session_id.0)
236 )]
237 pub async fn kill(
238 &self,
239 terminal_id: impl Into<TerminalId>,
240 ) -> Result<KillTerminalCommandResponse, AgentError> {
241 let start_time = Instant::now();
242 let tid: TerminalId = terminal_id.into();
243
244 tracing::info!(
245 terminal_id = %tid.0,
246 "Killing terminal command"
247 );
248
249 let request = KillTerminalCommandRequest::new(self.session_id.clone(), tid.clone());
250
251 let response = self
252 .connection_cx
253 .send_request(request)
254 .block_task()
255 .await
256 .map_err(|e| {
257 let elapsed = start_time.elapsed();
258 tracing::error!(
259 terminal_id = %tid.0,
260 error = %e,
261 elapsed_ms = elapsed.as_millis(),
262 "Terminal kill failed"
263 );
264 AgentError::Internal(format!("Terminal kill failed: {}", e))
265 })?;
266
267 let elapsed = start_time.elapsed();
268 tracing::info!(
269 terminal_id = %tid.0,
270 elapsed_ms = elapsed.as_millis(),
271 "Terminal command killed"
272 );
273
274 Ok(response)
275 }
276
277 #[instrument(
282 name = "terminal_release",
283 skip(self, terminal_id),
284 fields(session_id = %self.session_id.0)
285 )]
286 pub async fn release(
287 &self,
288 terminal_id: impl Into<TerminalId>,
289 ) -> Result<ReleaseTerminalResponse, AgentError> {
290 let start_time = Instant::now();
291 let tid: TerminalId = terminal_id.into();
292
293 tracing::debug!(
294 terminal_id = %tid.0,
295 "Releasing terminal"
296 );
297
298 let request = ReleaseTerminalRequest::new(self.session_id.clone(), tid.clone());
299
300 let response = self
301 .connection_cx
302 .send_request(request)
303 .block_task()
304 .await
305 .map_err(|e| {
306 let elapsed = start_time.elapsed();
307 tracing::error!(
308 terminal_id = %tid.0,
309 error = %e,
310 elapsed_ms = elapsed.as_millis(),
311 "Terminal release failed"
312 );
313 AgentError::Internal(format!("Terminal release failed: {}", e))
314 })?;
315
316 let elapsed = start_time.elapsed();
317 tracing::debug!(
318 terminal_id = %tid.0,
319 elapsed_ms = elapsed.as_millis(),
320 "Terminal released"
321 );
322
323 Ok(response)
324 }
325
326 pub fn session_id(&self) -> &SessionId {
328 &self.session_id
329 }
330
331 pub fn into_arc(self) -> Arc<Self> {
333 Arc::new(self)
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 #[test]
340 fn test_terminal_client_session_id() {
341 }
344}