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
58fn 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}