claude_code_acp/terminal/
handle.rs

1//! Terminal handle for managing an active terminal session
2//!
3//! Provides a convenient RAII wrapper around a terminal ID that automatically
4//! releases the terminal when dropped.
5
6use std::sync::Arc;
7
8use sacp::schema::{TerminalExitStatus, TerminalId, TerminalOutputResponse};
9
10use super::TerminalClient;
11use crate::types::AgentError;
12
13/// Handle to an active terminal session
14///
15/// This provides a convenient wrapper around a `TerminalId` that tracks
16/// the terminal client and can be used to interact with the terminal.
17///
18/// When dropped, the handle will attempt to release the terminal if it
19/// hasn't been explicitly released or if the command hasn't completed.
20#[derive(Debug)]
21pub struct TerminalHandle {
22    /// The terminal ID
23    terminal_id: TerminalId,
24    /// The terminal client for sending requests
25    client: Arc<TerminalClient>,
26    /// Whether the terminal has been released
27    released: bool,
28}
29
30impl TerminalHandle {
31    /// Create a new terminal handle
32    pub fn new(terminal_id: TerminalId, client: Arc<TerminalClient>) -> Self {
33        Self {
34            terminal_id,
35            client,
36            released: false,
37        }
38    }
39
40    /// Get the terminal ID
41    pub fn id(&self) -> &TerminalId {
42        &self.terminal_id
43    }
44
45    /// Get the terminal ID as a string
46    pub fn id_str(&self) -> &str {
47        self.terminal_id.0.as_ref()
48    }
49
50    /// Get the current output and status
51    pub async fn output(&self) -> Result<TerminalOutputResponse, AgentError> {
52        self.client.output(self.terminal_id.clone()).await
53    }
54
55    /// Wait for the terminal command to exit
56    ///
57    /// Returns the exit status once the command completes.
58    pub async fn wait_for_exit(&self) -> Result<TerminalExitStatus, AgentError> {
59        let response = self.client.wait_for_exit(self.terminal_id.clone()).await?;
60        Ok(response.exit_status)
61    }
62
63    /// Kill the terminal command
64    ///
65    /// Sends SIGTERM to terminate the command. The terminal remains valid
66    /// and can still be queried for output.
67    pub async fn kill(&self) -> Result<(), AgentError> {
68        self.client.kill(self.terminal_id.clone()).await?;
69        Ok(())
70    }
71
72    /// Release the terminal and free resources
73    ///
74    /// After calling this, the handle should not be used again.
75    pub async fn release(mut self) -> Result<(), AgentError> {
76        self.released = true;
77        self.client.release(self.terminal_id.clone()).await?;
78        Ok(())
79    }
80
81    /// Execute a command and wait for completion, returning the output
82    ///
83    /// This is a convenience method that polls for output and waits for exit.
84    /// Returns the final output and exit status.
85    pub async fn execute_and_wait(&self) -> Result<(String, TerminalExitStatus), AgentError> {
86        // Wait for the command to exit
87        let exit_status = self.wait_for_exit().await?;
88
89        // Get the final output
90        let output_response = self.output().await?;
91
92        Ok((output_response.output, exit_status))
93    }
94
95    /// Check if the terminal has been released
96    pub fn is_released(&self) -> bool {
97        self.released
98    }
99}
100
101// Note: We don't implement Drop with async release because that's complex.
102// Users should explicitly call release() when done, or let the terminal
103// time out on the client side.
104
105impl Drop for TerminalHandle {
106    fn drop(&mut self) {
107        if !self.released {
108            tracing::warn!(
109                terminal_id = %self.id_str(),
110                "TerminalHandle dropped without explicit release, \
111                 terminal will be cleaned up by client timeout"
112            );
113        }
114    }
115}
116
117/// Builder for creating terminals with various options
118#[derive(Debug)]
119#[allow(dead_code)] // Public API for future use
120pub struct TerminalBuilder {
121    client: Arc<TerminalClient>,
122    command: String,
123    args: Vec<String>,
124    cwd: Option<std::path::PathBuf>,
125    output_byte_limit: Option<u64>,
126}
127
128#[allow(dead_code)] // Public API for future use
129impl TerminalBuilder {
130    /// Create a new terminal builder
131    pub fn new(client: Arc<TerminalClient>, command: impl Into<String>) -> Self {
132        Self {
133            client,
134            command: command.into(),
135            args: Vec::new(),
136            cwd: None,
137            output_byte_limit: None,
138        }
139    }
140
141    /// Set command arguments
142    pub fn args(mut self, args: Vec<String>) -> Self {
143        self.args = args;
144        self
145    }
146
147    /// Add a single argument
148    pub fn arg(mut self, arg: impl Into<String>) -> Self {
149        self.args.push(arg.into());
150        self
151    }
152
153    /// Set the working directory
154    pub fn cwd(mut self, cwd: impl Into<std::path::PathBuf>) -> Self {
155        self.cwd = Some(cwd.into());
156        self
157    }
158
159    /// Set the output byte limit
160    pub fn output_byte_limit(mut self, limit: u64) -> Self {
161        self.output_byte_limit = Some(limit);
162        self
163    }
164
165    /// Create the terminal
166    pub async fn create(self) -> Result<TerminalHandle, AgentError> {
167        let terminal_id = self
168            .client
169            .create(self.command, self.args, self.cwd, self.output_byte_limit)
170            .await?;
171
172        Ok(TerminalHandle::new(terminal_id, self.client))
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    // Tests would require mocking the JrConnectionCx which is complex
179    // For now, we just verify the types compile correctly
180}