use std::path::PathBuf;
use std::sync::Arc;
use std::time::Instant;
use sacp::JrConnectionCx;
use sacp::link::AgentToClient;
use sacp::schema::{
CreateTerminalRequest, CreateTerminalResponse, EnvVariable, KillTerminalCommandRequest,
KillTerminalCommandResponse, ReleaseTerminalRequest, ReleaseTerminalResponse, SessionId,
TerminalId, TerminalOutputRequest, TerminalOutputResponse, WaitForTerminalExitRequest,
WaitForTerminalExitResponse,
};
use tracing::instrument;
use crate::types::AgentError;
#[derive(Debug, Clone)]
pub struct TerminalClient {
connection_cx: JrConnectionCx<AgentToClient>,
session_id: SessionId,
}
impl TerminalClient {
pub fn new(
connection_cx: JrConnectionCx<AgentToClient>,
session_id: impl Into<SessionId>,
) -> Self {
Self {
connection_cx,
session_id: session_id.into(),
}
}
#[instrument(
name = "terminal_create",
skip(self, command, args, cwd),
fields(
session_id = %self.session_id.0,
args_count = args.len(),
has_cwd = cwd.is_some(),
)
)]
pub async fn create(
&self,
command: impl Into<String>,
args: Vec<String>,
cwd: Option<PathBuf>,
output_byte_limit: Option<u64>,
) -> Result<TerminalId, AgentError> {
let start_time = Instant::now();
let cmd: String = command.into();
tracing::info!(
command = %cmd,
args = ?args,
cwd = ?cwd,
output_byte_limit = ?output_byte_limit,
"Creating terminal and executing command"
);
let mut request = CreateTerminalRequest::new(self.session_id.clone(), cmd.clone());
request = request.args(args.clone());
request = request.env(vec![EnvVariable::new("CLAUDECODE", "1")]);
if let Some(cwd_path) = cwd.clone() {
request = request.cwd(cwd_path);
}
if let Some(limit) = output_byte_limit {
request = request.output_byte_limit(limit);
}
tracing::debug!("Sending terminal/create request to ACP client");
let response: CreateTerminalResponse = self
.connection_cx
.send_request(request)
.block_task()
.await
.map_err(|e| {
let elapsed = start_time.elapsed();
tracing::error!(
session_id = %self.session_id.0,
command = %cmd,
error = %e,
error_type = %std::any::type_name::<sacp::Error>(),
elapsed_ms = elapsed.as_millis(),
"Terminal create request failed"
);
AgentError::Internal(format!("Terminal create failed: {}", e))
})?;
let elapsed = start_time.elapsed();
tracing::info!(
terminal_id = %response.terminal_id.0,
command = %cmd,
elapsed_ms = elapsed.as_millis(),
"Terminal created successfully"
);
Ok(response.terminal_id)
}
#[instrument(
name = "terminal_output",
skip(self, terminal_id),
fields(session_id = %self.session_id.0)
)]
pub async fn output(
&self,
terminal_id: impl Into<TerminalId>,
) -> Result<TerminalOutputResponse, AgentError> {
let start_time = Instant::now();
let tid: TerminalId = terminal_id.into();
tracing::debug!(
terminal_id = %tid.0,
"Getting terminal output"
);
let request = TerminalOutputRequest::new(self.session_id.clone(), tid.clone());
let response = self
.connection_cx
.send_request(request)
.block_task()
.await
.map_err(|e| {
let elapsed = start_time.elapsed();
tracing::error!(
terminal_id = %tid.0,
error = %e,
elapsed_ms = elapsed.as_millis(),
"Terminal output request failed"
);
AgentError::Internal(format!("Terminal output failed: {}", e))
})?;
let elapsed = start_time.elapsed();
tracing::debug!(
terminal_id = %tid.0,
elapsed_ms = elapsed.as_millis(),
output_len = response.output.len(),
exit_status = ?response.exit_status,
"Terminal output retrieved"
);
Ok(response)
}
#[instrument(
name = "terminal_wait_for_exit",
skip(self, terminal_id),
fields(session_id = %self.session_id.0)
)]
pub async fn wait_for_exit(
&self,
terminal_id: impl Into<TerminalId>,
) -> Result<WaitForTerminalExitResponse, AgentError> {
let start_time = Instant::now();
let tid: TerminalId = terminal_id.into();
tracing::info!(
terminal_id = %tid.0,
"Waiting for terminal command to exit"
);
let request = WaitForTerminalExitRequest::new(self.session_id.clone(), tid.clone());
let response = self
.connection_cx
.send_request(request)
.block_task()
.await
.map_err(|e| {
let elapsed = start_time.elapsed();
tracing::error!(
terminal_id = %tid.0,
error = %e,
elapsed_ms = elapsed.as_millis(),
"Terminal wait_for_exit failed"
);
AgentError::Internal(format!("Terminal wait_for_exit failed: {}", e))
})?;
let elapsed = start_time.elapsed();
tracing::info!(
terminal_id = %tid.0,
elapsed_ms = elapsed.as_millis(),
exit_status = ?response.exit_status,
"Terminal command exited"
);
Ok(response)
}
#[instrument(
name = "terminal_kill",
skip(self, terminal_id),
fields(session_id = %self.session_id.0)
)]
pub async fn kill(
&self,
terminal_id: impl Into<TerminalId>,
) -> Result<KillTerminalCommandResponse, AgentError> {
let start_time = Instant::now();
let tid: TerminalId = terminal_id.into();
tracing::info!(
terminal_id = %tid.0,
"Killing terminal command"
);
let request = KillTerminalCommandRequest::new(self.session_id.clone(), tid.clone());
let response = self
.connection_cx
.send_request(request)
.block_task()
.await
.map_err(|e| {
let elapsed = start_time.elapsed();
tracing::error!(
terminal_id = %tid.0,
error = %e,
elapsed_ms = elapsed.as_millis(),
"Terminal kill failed"
);
AgentError::Internal(format!("Terminal kill failed: {}", e))
})?;
let elapsed = start_time.elapsed();
tracing::info!(
terminal_id = %tid.0,
elapsed_ms = elapsed.as_millis(),
"Terminal command killed"
);
Ok(response)
}
#[instrument(
name = "terminal_release",
skip(self, terminal_id),
fields(session_id = %self.session_id.0)
)]
pub async fn release(
&self,
terminal_id: impl Into<TerminalId>,
) -> Result<ReleaseTerminalResponse, AgentError> {
let start_time = Instant::now();
let tid: TerminalId = terminal_id.into();
tracing::debug!(
terminal_id = %tid.0,
"Releasing terminal"
);
let request = ReleaseTerminalRequest::new(self.session_id.clone(), tid.clone());
let response = self
.connection_cx
.send_request(request)
.block_task()
.await
.map_err(|e| {
let elapsed = start_time.elapsed();
tracing::error!(
terminal_id = %tid.0,
error = %e,
elapsed_ms = elapsed.as_millis(),
"Terminal release failed"
);
AgentError::Internal(format!("Terminal release failed: {}", e))
})?;
let elapsed = start_time.elapsed();
tracing::debug!(
terminal_id = %tid.0,
elapsed_ms = elapsed.as_millis(),
"Terminal released"
);
Ok(response)
}
pub fn session_id(&self) -> &SessionId {
&self.session_id
}
pub fn into_arc(self) -> Arc<Self> {
Arc::new(self)
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_terminal_client_session_id() {
}
}