ricecoder_commands/
output_injection.rs

1use crate::error::Result;
2use crate::types::CommandExecutionResult;
3
4/// Output injection configuration
5#[derive(Debug, Clone)]
6pub struct OutputInjectionConfig {
7    /// Whether to inject stdout
8    pub inject_stdout: bool,
9
10    /// Whether to inject stderr
11    pub inject_stderr: bool,
12
13    /// Maximum output length (0 = unlimited)
14    pub max_length: usize,
15
16    /// Whether to include exit code in output
17    pub include_exit_code: bool,
18
19    /// Whether to include execution time in output
20    pub include_duration: bool,
21
22    /// Format for injected output
23    pub format: OutputFormat,
24}
25
26/// Output format for injection
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum OutputFormat {
29    /// Plain text format
30    Plain,
31
32    /// Markdown format with code blocks
33    Markdown,
34
35    /// JSON format
36    Json,
37}
38
39/// Output injector for command results
40pub struct OutputInjector;
41
42impl OutputInjector {
43    /// Inject command output into chat format
44    pub fn inject(
45        result: &CommandExecutionResult,
46        config: &OutputInjectionConfig,
47    ) -> Result<String> {
48        match config.format {
49            OutputFormat::Plain => Self::format_plain(result, config),
50            OutputFormat::Markdown => Self::format_markdown(result, config),
51            OutputFormat::Json => Self::format_json(result, config),
52        }
53    }
54
55    /// Format output as plain text
56    fn format_plain(
57        result: &CommandExecutionResult,
58        config: &OutputInjectionConfig,
59    ) -> Result<String> {
60        let mut output = String::new();
61
62        if config.include_exit_code {
63            output.push_str(&format!("Exit Code: {}\n", result.exit_code));
64        }
65
66        if config.include_duration {
67            output.push_str(&format!("Duration: {}ms\n", result.duration_ms));
68        }
69
70        if config.inject_stdout && !result.stdout.is_empty() {
71            let stdout = if config.max_length > 0 && result.stdout.len() > config.max_length {
72                format!("{}... (truncated)", &result.stdout[..config.max_length])
73            } else {
74                result.stdout.clone()
75            };
76            output.push_str(&format!("Output:\n{}\n", stdout));
77        }
78
79        if config.inject_stderr && !result.stderr.is_empty() {
80            let stderr = if config.max_length > 0 && result.stderr.len() > config.max_length {
81                format!("{}... (truncated)", &result.stderr[..config.max_length])
82            } else {
83                result.stderr.clone()
84            };
85            output.push_str(&format!("Error:\n{}\n", stderr));
86        }
87
88        Ok(output.trim().to_string())
89    }
90
91    /// Format output as markdown
92    fn format_markdown(
93        result: &CommandExecutionResult,
94        config: &OutputInjectionConfig,
95    ) -> Result<String> {
96        let mut output = String::new();
97
98        if config.include_exit_code {
99            output.push_str(&format!("**Exit Code:** {}\n", result.exit_code));
100        }
101
102        if config.include_duration {
103            output.push_str(&format!("**Duration:** {}ms\n", result.duration_ms));
104        }
105
106        if config.inject_stdout && !result.stdout.is_empty() {
107            let stdout = if config.max_length > 0 && result.stdout.len() > config.max_length {
108                format!("{}... (truncated)", &result.stdout[..config.max_length])
109            } else {
110                result.stdout.clone()
111            };
112            output.push_str(&format!("```\n{}\n```\n", stdout));
113        }
114
115        if config.inject_stderr && !result.stderr.is_empty() {
116            let stderr = if config.max_length > 0 && result.stderr.len() > config.max_length {
117                format!("{}... (truncated)", &result.stderr[..config.max_length])
118            } else {
119                result.stderr.clone()
120            };
121            output.push_str(&format!("**Error:**\n```\n{}\n```\n", stderr));
122        }
123
124        Ok(output.trim().to_string())
125    }
126
127    /// Format output as JSON
128    fn format_json(
129        result: &CommandExecutionResult,
130        config: &OutputInjectionConfig,
131    ) -> Result<String> {
132        let mut json = serde_json::json!({
133            "command_id": result.command_id,
134            "exit_code": result.exit_code,
135            "success": result.success,
136        });
137
138        if config.include_duration {
139            json["duration_ms"] = serde_json::json!(result.duration_ms);
140        }
141
142        if config.inject_stdout {
143            let stdout = if config.max_length > 0 && result.stdout.len() > config.max_length {
144                format!("{}... (truncated)", &result.stdout[..config.max_length])
145            } else {
146                result.stdout.clone()
147            };
148            json["stdout"] = serde_json::json!(stdout);
149        }
150
151        if config.inject_stderr {
152            let stderr = if config.max_length > 0 && result.stderr.len() > config.max_length {
153                format!("{}... (truncated)", &result.stderr[..config.max_length])
154            } else {
155                result.stderr.clone()
156            };
157            json["stderr"] = serde_json::json!(stderr);
158        }
159
160        Ok(serde_json::to_string_pretty(&json)?)
161    }
162}
163
164impl Default for OutputInjectionConfig {
165    fn default() -> Self {
166        Self {
167            inject_stdout: true,
168            inject_stderr: true,
169            max_length: 5000,
170            include_exit_code: true,
171            include_duration: true,
172            format: OutputFormat::Markdown,
173        }
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    fn create_test_result() -> CommandExecutionResult {
182        CommandExecutionResult::new("test-cmd", 0)
183            .with_stdout("Hello World")
184            .with_stderr("")
185            .with_duration(100)
186    }
187
188    #[test]
189    fn test_format_plain() {
190        let result = create_test_result();
191        let config = OutputInjectionConfig {
192            format: OutputFormat::Plain,
193            ..Default::default()
194        };
195
196        let output = OutputInjector::inject(&result, &config).unwrap();
197        assert!(output.contains("Exit Code: 0"));
198        assert!(output.contains("Duration: 100ms"));
199        assert!(output.contains("Hello World"));
200    }
201
202    #[test]
203    fn test_format_markdown() {
204        let result = create_test_result();
205        let config = OutputInjectionConfig {
206            format: OutputFormat::Markdown,
207            ..Default::default()
208        };
209
210        let output = OutputInjector::inject(&result, &config).unwrap();
211        assert!(output.contains("**Exit Code:** 0"));
212        assert!(output.contains("**Duration:** 100ms"));
213        assert!(output.contains("```"));
214        assert!(output.contains("Hello World"));
215    }
216
217    #[test]
218    fn test_format_json() {
219        let result = create_test_result();
220        let config = OutputInjectionConfig {
221            format: OutputFormat::Json,
222            ..Default::default()
223        };
224
225        let output = OutputInjector::inject(&result, &config).unwrap();
226        let json: serde_json::Value = serde_json::from_str(&output).unwrap();
227        assert_eq!(json["command_id"], "test-cmd");
228        assert_eq!(json["exit_code"], 0);
229        assert!(json["success"].as_bool().unwrap());
230    }
231
232    #[test]
233    fn test_truncate_output() {
234        let mut result = create_test_result();
235        result.stdout = "a".repeat(10000);
236
237        let config = OutputInjectionConfig {
238            format: OutputFormat::Plain,
239            max_length: 100,
240            ..Default::default()
241        };
242
243        let output = OutputInjector::inject(&result, &config).unwrap();
244        assert!(output.contains("(truncated)"));
245        assert!(!output.contains(&"a".repeat(10000)));
246    }
247
248    #[test]
249    fn test_exclude_stderr() {
250        let mut result = create_test_result();
251        result.stderr = "Error message".to_string();
252
253        let config = OutputInjectionConfig {
254            format: OutputFormat::Plain,
255            inject_stderr: false,
256            ..Default::default()
257        };
258
259        let output = OutputInjector::inject(&result, &config).unwrap();
260        assert!(!output.contains("Error message"));
261    }
262
263    #[test]
264    fn test_exclude_exit_code() {
265        let result = create_test_result();
266        let config = OutputInjectionConfig {
267            format: OutputFormat::Plain,
268            include_exit_code: false,
269            ..Default::default()
270        };
271
272        let output = OutputInjector::inject(&result, &config).unwrap();
273        assert!(!output.contains("Exit Code"));
274    }
275}