claude_code_acp/terminal/
client.rs

1//! Terminal API client
2//!
3//! Provides a client interface for sending Terminal API requests to the ACP Client.
4
5use std::path::PathBuf;
6use std::sync::Arc;
7
8use sacp::link::AgentToClient;
9use sacp::schema::{
10    CreateTerminalRequest, CreateTerminalResponse, EnvVariable, KillTerminalCommandRequest,
11    KillTerminalCommandResponse, ReleaseTerminalRequest, ReleaseTerminalResponse, SessionId,
12    TerminalId, TerminalOutputRequest, TerminalOutputResponse, WaitForTerminalExitRequest,
13    WaitForTerminalExitResponse,
14};
15use sacp::JrConnectionCx;
16
17use crate::types::AgentError;
18
19/// Terminal API client for sending terminal requests to the ACP Client
20///
21/// The Client (editor like Zed) manages the actual PTY, and this client
22/// sends requests through the ACP protocol to create, manage, and interact
23/// with terminals.
24#[derive(Debug, Clone)]
25pub struct TerminalClient {
26    /// Connection context for sending requests
27    connection_cx: JrConnectionCx<AgentToClient>,
28    /// Session ID for this client
29    session_id: SessionId,
30}
31
32impl TerminalClient {
33    /// Create a new Terminal API client
34    pub fn new(connection_cx: JrConnectionCx<AgentToClient>, session_id: impl Into<SessionId>) -> Self {
35        Self {
36            connection_cx,
37            session_id: session_id.into(),
38        }
39    }
40
41    /// Create a new terminal and execute a command
42    ///
43    /// Returns a `TerminalId` that can be used with other terminal methods.
44    /// The terminal will execute the specified command and capture output.
45    ///
46    /// # Arguments
47    ///
48    /// * `command` - The command to execute
49    /// * `args` - Command arguments
50    /// * `cwd` - Optional working directory (uses session cwd if not specified)
51    /// * `output_byte_limit` - Optional limit on output bytes to retain
52    pub async fn create(
53        &self,
54        command: impl Into<String>,
55        args: Vec<String>,
56        cwd: Option<PathBuf>,
57        output_byte_limit: Option<u64>,
58    ) -> Result<TerminalId, AgentError> {
59        let mut request = CreateTerminalRequest::new(self.session_id.clone(), command);
60        request = request.args(args);
61
62        // Set CLAUDECODE environment variable (required by some clients like Zed)
63        request = request.env(vec![EnvVariable::new("CLAUDECODE", "1")]);
64
65        if let Some(cwd_path) = cwd {
66            request = request.cwd(cwd_path);
67        }
68
69        if let Some(limit) = output_byte_limit {
70            request = request.output_byte_limit(limit);
71        }
72
73        tracing::debug!(?request, "Sending terminal/create request");
74
75        let response: CreateTerminalResponse = self
76            .connection_cx
77            .send_request(request)
78            .block_task()
79            .await
80            .map_err(|e| AgentError::Internal(format!("Terminal create failed: {}", e)))?;
81
82        tracing::debug!(?response, "Received terminal/create response");
83
84        Ok(response.terminal_id)
85    }
86
87    /// Get the current output and status of a terminal
88    ///
89    /// Returns the output captured so far and the exit status if completed.
90    pub async fn output(
91        &self,
92        terminal_id: impl Into<TerminalId>,
93    ) -> Result<TerminalOutputResponse, AgentError> {
94        let request = TerminalOutputRequest::new(self.session_id.clone(), terminal_id);
95
96        self.connection_cx
97            .send_request(request)
98            .block_task()
99            .await
100            .map_err(|e| AgentError::Internal(format!("Terminal output failed: {}", e)))
101    }
102
103    /// Wait for a terminal command to exit
104    ///
105    /// Blocks until the command completes and returns the exit status.
106    pub async fn wait_for_exit(
107        &self,
108        terminal_id: impl Into<TerminalId>,
109    ) -> Result<WaitForTerminalExitResponse, AgentError> {
110        let request = WaitForTerminalExitRequest::new(self.session_id.clone(), terminal_id);
111
112        self.connection_cx
113            .send_request(request)
114            .block_task()
115            .await
116            .map_err(|e| AgentError::Internal(format!("Terminal wait_for_exit failed: {}", e)))
117    }
118
119    /// Kill a terminal command
120    ///
121    /// Sends SIGTERM to terminate the command. The terminal remains valid
122    /// and can be queried for output or released.
123    pub async fn kill(
124        &self,
125        terminal_id: impl Into<TerminalId>,
126    ) -> Result<KillTerminalCommandResponse, AgentError> {
127        let request = KillTerminalCommandRequest::new(self.session_id.clone(), terminal_id);
128
129        self.connection_cx
130            .send_request(request)
131            .block_task()
132            .await
133            .map_err(|e| AgentError::Internal(format!("Terminal kill failed: {}", e)))
134    }
135
136    /// Release a terminal and free its resources
137    ///
138    /// After release, the `TerminalId` can no longer be used.
139    /// Any unretrieved output will be lost.
140    pub async fn release(
141        &self,
142        terminal_id: impl Into<TerminalId>,
143    ) -> Result<ReleaseTerminalResponse, AgentError> {
144        let request = ReleaseTerminalRequest::new(self.session_id.clone(), terminal_id);
145
146        self.connection_cx
147            .send_request(request)
148            .block_task()
149            .await
150            .map_err(|e| AgentError::Internal(format!("Terminal release failed: {}", e)))
151    }
152
153    /// Get the session ID
154    pub fn session_id(&self) -> &SessionId {
155        &self.session_id
156    }
157
158    /// Create an Arc-wrapped client for sharing
159    pub fn into_arc(self) -> Arc<Self> {
160        Arc::new(self)
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    #[test]
167    fn test_terminal_client_session_id() {
168        // We can't easily test without a real connection, but we can verify the struct compiles
169        // and the session_id method works (would need mock connection for full test)
170    }
171}