Skip to main content

acp_cli/output/
json.rs

1use std::io::{self, Write};
2
3use serde_json::json;
4
5use super::OutputRenderer;
6
7pub struct JsonRenderer {
8    suppress_reads: bool,
9}
10
11impl JsonRenderer {
12    pub fn new(suppress_reads: bool) -> Self {
13        Self { suppress_reads }
14    }
15
16    fn emit(&self, value: serde_json::Value) {
17        let line = serde_json::to_string(&value).expect("failed to serialize JSON output");
18        println!("{line}");
19        let _ = io::stdout().flush();
20    }
21}
22
23impl OutputRenderer for JsonRenderer {
24    fn text_chunk(&mut self, text: &str) {
25        self.emit(json!({"type": "text", "content": text}));
26    }
27
28    fn tool_status(&mut self, tool: &str) {
29        self.emit(json!({"type": "tool", "name": tool}));
30    }
31
32    fn tool_result(&mut self, tool: &str, output: &str, is_read: bool) {
33        self.emit(build_tool_result_event(
34            tool,
35            output,
36            self.suppress_reads,
37            is_read,
38        ));
39    }
40
41    fn permission_denied(&mut self, tool: &str) {
42        self.emit(json!({"type": "error", "message": format!("permission denied: {tool}")}));
43    }
44
45    fn error(&mut self, err: &str) {
46        self.emit(json!({"type": "error", "message": err}));
47    }
48
49    fn session_info(&mut self, id: &str) {
50        self.emit(json!({"type": "session", "sessionId": id}));
51    }
52
53    fn done(&mut self) {
54        self.emit(json!({"type": "done"}));
55    }
56}
57
58/// Build the JSON value for a `tool_result` event. Extracted for testability.
59fn build_tool_result_event(
60    tool: &str,
61    output: &str,
62    suppress_reads: bool,
63    is_read: bool,
64) -> serde_json::Value {
65    if suppress_reads && is_read {
66        json!({
67            "type": "tool_result",
68            "name": tool,
69            "output": "[suppressed]",
70            "suppressed": true,
71        })
72    } else {
73        json!({
74            "type": "tool_result",
75            "name": tool,
76            "output": output,
77        })
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn tool_result_suppressed_when_suppress_reads_and_is_read() {
87        let v = build_tool_result_event("Read File", "file contents", true, true);
88        assert_eq!(v["type"], "tool_result");
89        assert_eq!(v["name"], "Read File");
90        assert_eq!(v["output"], "[suppressed]");
91        assert_eq!(v["suppressed"], true);
92    }
93
94    #[test]
95    fn tool_result_not_suppressed_when_is_read_false() {
96        let v = build_tool_result_event("Bash", "output text", true, false);
97        assert_eq!(v["output"], "output text");
98        assert!(v.get("suppressed").is_none());
99    }
100
101    #[test]
102    fn tool_result_not_suppressed_when_suppress_reads_false() {
103        let v = build_tool_result_event("Read File", "file contents", false, true);
104        assert_eq!(v["output"], "file contents");
105        assert!(v.get("suppressed").is_none());
106    }
107}