Skip to main content
Glama
snapshot.rs6.46 kB
//! Terminal snapshot functionality. use std::time::{Duration, Instant}; use terminal_mcp_core::{Result, TerminalStateTree}; use terminal_mcp_detector::{DetectionPipeline, TSTAssembler}; use crate::session::Session; /// Configuration for snapshot operations. #[derive(Debug, Clone)] pub struct SnapshotConfig { /// Maximum time to wait for idle before taking snapshot pub idle_timeout: Duration, /// Time to consider terminal "idle" (no output received) pub idle_threshold: Duration, /// Maximum number of bytes to process per iteration pub max_bytes_per_iteration: usize, } impl Default for SnapshotConfig { fn default() -> Self { Self { idle_timeout: Duration::from_secs(5), idle_threshold: Duration::from_millis(100), max_bytes_per_iteration: 4096, } } } impl Session { /// Capture a snapshot of the terminal state. /// /// This waits for the terminal to become idle (no output for idle_threshold), /// then captures the current grid state and runs detection to build a /// Terminal State Tree. pub fn snapshot( &self, pipeline: &DetectionPipeline, config: &SnapshotConfig, ) -> Result<TerminalStateTree> { // Wait for idle self.wait_for_idle(config)?; // Get grid state let parser_arc = self.parser(); let parser = parser_arc.lock().unwrap(); let grid = parser.grid(); let cursor = grid.cursor().position; let dimensions = grid.dimensions(); let raw_text = grid.to_plain_text(); // Run detection pipeline let detected = pipeline.detect(grid, cursor); // Build TST let assembler = TSTAssembler::new(); let tst = assembler.assemble( detected, self.id().to_string(), dimensions, cursor, raw_text, ); Ok(tst) } /// Wait for terminal to become idle. /// /// Continuously processes PTY output until no new output is received /// for the configured idle_threshold duration, or until idle_timeout is reached. fn wait_for_idle(&self, config: &SnapshotConfig) -> Result<()> { let start = Instant::now(); let mut last_output = Instant::now(); loop { // Check timeout if start.elapsed() > config.idle_timeout { break; } // Process available output let bytes_read = self.process_output()?; if bytes_read > 0 { // Reset idle timer last_output = Instant::now(); } else { // Check if idle long enough if last_output.elapsed() >= config.idle_threshold { break; } } // Small sleep to avoid busy-waiting std::thread::sleep(Duration::from_millis(10)); } Ok(()) } } #[cfg(test)] mod tests { use super::*; use terminal_mcp_core::Dimensions; #[test] fn test_snapshot_config_default() { let config = SnapshotConfig::default(); assert_eq!(config.idle_timeout, Duration::from_secs(5)); assert_eq!(config.idle_threshold, Duration::from_millis(100)); assert_eq!(config.max_bytes_per_iteration, 4096); } #[test] fn test_snapshot() { let session = Session::create( "echo".to_string(), vec!["test".to_string()], Dimensions::new(24, 80), ) .unwrap(); let pipeline = DetectionPipeline::new(); let config = SnapshotConfig::default(); // Allow some time for command to execute std::thread::sleep(Duration::from_millis(200)); let result = session.snapshot(&pipeline, &config); assert!(result.is_ok()); let tst = result.unwrap(); assert_eq!(tst.session_id, session.id().to_string()); assert_eq!(tst.dimensions, Dimensions::new(24, 80)); } #[test] fn test_wait_for_idle() { let session = Session::create( "echo".to_string(), vec!["test".to_string()], Dimensions::new(24, 80), ) .unwrap(); let config = SnapshotConfig { idle_timeout: Duration::from_secs(2), idle_threshold: Duration::from_millis(100), max_bytes_per_iteration: 4096, }; let start = Instant::now(); let result = session.wait_for_idle(&config); let elapsed = start.elapsed(); assert!(result.is_ok()); // Should complete within idle_timeout assert!(elapsed < config.idle_timeout + Duration::from_millis(500)); } #[test] fn test_snapshot_with_custom_config() { let session = Session::create( "echo".to_string(), vec!["hello".to_string()], Dimensions::new(24, 80), ) .unwrap(); let pipeline = DetectionPipeline::new(); let config = SnapshotConfig { idle_timeout: Duration::from_secs(2), idle_threshold: Duration::from_millis(50), max_bytes_per_iteration: 2048, }; std::thread::sleep(Duration::from_millis(200)); let result = session.snapshot(&pipeline, &config); assert!(result.is_ok()); } #[test] fn test_snapshot_with_idle_bash_no_hang() { // Regression test for #99: terminal_snapshot should not hang on idle shells let session = Session::create("bash".to_string(), vec![], Dimensions::new(24, 80)).unwrap(); let pipeline = DetectionPipeline::new(); let config = SnapshotConfig { idle_threshold: Duration::from_millis(500), idle_timeout: Duration::from_secs(3), ..Default::default() }; // Let bash start and reach idle prompt std::thread::sleep(Duration::from_millis(500)); // This should complete within idle_timeout, not hang indefinitely let start = Instant::now(); let result = session.snapshot(&pipeline, &config); let elapsed = start.elapsed(); assert!(result.is_ok(), "Snapshot should succeed on idle bash"); assert!( elapsed < Duration::from_secs(3), "Snapshot should complete within timeout, not hang (elapsed: {:?})", elapsed ); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aybelatchane/mcp-server-terminal'

If you have feedback or need assistance with the MCP directory API, please join our Discord server