Skip to main content

par_term/ai_inspector/
snapshot.rs

1use serde::{Deserialize, Serialize};
2
3/// Scope for terminal state snapshots.
4#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(rename_all = "snake_case")]
6pub enum SnapshotScope {
7    Visible,
8    Recent(usize),
9    Full,
10}
11
12impl SnapshotScope {
13    pub fn from_config_str(s: &str) -> Self {
14        if s == "visible" {
15            Self::Visible
16        } else if s == "full" {
17            Self::Full
18        } else if let Some(n) = s.strip_prefix("recent_") {
19            Self::Recent(n.parse().unwrap_or(10))
20        } else {
21            Self::Visible
22        }
23    }
24
25    pub fn to_config_str(&self) -> String {
26        match self {
27            Self::Visible => "visible".to_string(),
28            Self::Recent(n) => format!("recent_{n}"),
29            Self::Full => "full".to_string(),
30        }
31    }
32}
33
34/// A single command entry from shell integration.
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct CommandEntry {
37    pub command: String,
38    pub exit_code: Option<i32>,
39    pub duration_ms: u64,
40    pub cwd: Option<String>,
41    pub output: Option<String>,
42    pub output_line_count: usize,
43}
44
45/// Environment metadata from shell integration.
46#[derive(Debug, Clone, Default, Serialize, Deserialize)]
47pub struct EnvironmentInfo {
48    pub hostname: Option<String>,
49    pub username: Option<String>,
50    pub cwd: Option<String>,
51    pub shell: Option<String>,
52}
53
54/// Terminal dimensions and cursor state.
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct TerminalInfo {
57    pub cols: usize,
58    pub rows: usize,
59    pub cursor: (usize, usize),
60}
61
62/// Complete terminal state snapshot.
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct SnapshotData {
65    pub timestamp: String,
66    pub scope: String,
67    pub environment: EnvironmentInfo,
68    pub terminal: TerminalInfo,
69    pub commands: Vec<CommandEntry>,
70}
71
72impl SnapshotData {
73    pub fn to_json(&self) -> Result<String, serde_json::Error> {
74        serde_json::to_string_pretty(self)
75    }
76
77    /// Gather a snapshot from the terminal manager.
78    ///
79    /// Pulls command history, environment info, and terminal state from
80    /// the `TerminalManager` and packages it into a `SnapshotData` according
81    /// to the requested `scope`.
82    ///
83    /// # Arguments
84    /// * `terminal` - The terminal manager to read state from
85    /// * `scope` - Controls how much history to include
86    /// * `_max_output_lines` - Reserved for future per-command output capture
87    pub fn gather(
88        terminal: &par_term_terminal::TerminalManager,
89        scope: &SnapshotScope,
90        _max_output_lines: usize,
91    ) -> Self {
92        // Get command history from core library via shell integration.
93        // Each entry is (command_text, exit_code, duration_ms).
94        let history = terminal.core_command_history();
95
96        let commands_to_include: Vec<_> = match scope {
97            SnapshotScope::Visible => {
98                // Take recent commands (approximate visible window)
99                history.iter().rev().take(10).rev().cloned().collect()
100            }
101            SnapshotScope::Recent(n) => history.iter().rev().take(*n).rev().cloned().collect(),
102            SnapshotScope::Full => history,
103        };
104
105        let cwd = terminal.shell_integration_cwd();
106
107        let commands: Vec<CommandEntry> = commands_to_include
108            .into_iter()
109            .map(|(cmd, exit_code, duration_ms)| CommandEntry {
110                command: cmd,
111                exit_code,
112                duration_ms: duration_ms.unwrap_or(0),
113                cwd: cwd.clone(),
114                output: None,
115                output_line_count: 0,
116            })
117            .collect();
118
119        let (cursor_col, cursor_row) = terminal.cursor_position();
120
121        let shell = std::env::var("SHELL").ok();
122
123        let environment = EnvironmentInfo {
124            hostname: terminal.shell_integration_hostname(),
125            username: terminal.shell_integration_username(),
126            cwd,
127            shell,
128        };
129
130        let (cols, rows) = terminal.dimensions();
131
132        let terminal_info = TerminalInfo {
133            cols,
134            rows,
135            cursor: (cursor_col, cursor_row),
136        };
137
138        Self {
139            timestamp: chrono::Utc::now().to_rfc3339(),
140            scope: scope.to_config_str(),
141            environment,
142            terminal: terminal_info,
143            commands,
144        }
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn test_scope_from_config_str() {
154        assert_eq!(
155            SnapshotScope::from_config_str("visible"),
156            SnapshotScope::Visible
157        );
158        assert_eq!(SnapshotScope::from_config_str("full"), SnapshotScope::Full);
159        assert_eq!(
160            SnapshotScope::from_config_str("recent_10"),
161            SnapshotScope::Recent(10)
162        );
163        assert_eq!(
164            SnapshotScope::from_config_str("recent_25"),
165            SnapshotScope::Recent(25)
166        );
167        assert_eq!(
168            SnapshotScope::from_config_str("unknown"),
169            SnapshotScope::Visible
170        );
171    }
172
173    #[test]
174    fn test_scope_roundtrip() {
175        let scopes = vec![
176            SnapshotScope::Visible,
177            SnapshotScope::Full,
178            SnapshotScope::Recent(10),
179        ];
180        for scope in scopes {
181            let s = scope.to_config_str();
182            assert_eq!(SnapshotScope::from_config_str(&s), scope);
183        }
184    }
185
186    #[test]
187    fn test_snapshot_to_json() {
188        let snapshot = SnapshotData {
189            timestamp: "2026-02-15T10:00:00Z".to_string(),
190            scope: "visible".to_string(),
191            environment: EnvironmentInfo {
192                hostname: Some("test-host".to_string()),
193                username: Some("user".to_string()),
194                cwd: Some("/home/user".to_string()),
195                shell: Some("zsh".to_string()),
196            },
197            terminal: TerminalInfo {
198                cols: 80,
199                rows: 24,
200                cursor: (0, 0),
201            },
202            commands: vec![CommandEntry {
203                command: "echo hello".to_string(),
204                exit_code: Some(0),
205                duration_ms: 100,
206                cwd: Some("/home/user".to_string()),
207                output: Some("hello\n".to_string()),
208                output_line_count: 1,
209            }],
210        };
211        let json = snapshot.to_json().unwrap();
212        assert!(json.contains("echo hello"));
213        assert!(json.contains("test-host"));
214    }
215}