1use std::collections::{BTreeMap, BTreeSet};
2
3#[derive(Clone, Debug, PartialEq, Eq)]
4pub struct TextContent {
5 pub text: String,
6}
7
8#[derive(Clone, Debug, PartialEq, Eq)]
9pub struct ThinkingContent {
10 pub thinking: String,
11}
12
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct ToolCall {
15 pub id: String,
16 pub name: String,
17 pub arguments: String,
18}
19
20#[derive(Clone, Debug, PartialEq, Eq)]
21pub struct ToolResult {
22 pub tool_call_id: String,
23 pub content: String,
24 pub is_error: bool,
25}
26
27#[derive(Clone, Debug, PartialEq, Eq)]
28pub struct JsonEvent {
29 pub event_type: String,
30 pub text: String,
31 pub thinking: String,
32 pub tool_call: Option<ToolCall>,
33 pub tool_result: Option<ToolResult>,
34}
35
36#[derive(Clone, Debug, PartialEq)]
37pub struct ParsedJsonOutput {
38 pub schema_name: String,
39 pub events: Vec<JsonEvent>,
40 pub final_text: String,
41 pub session_id: String,
42 pub error: String,
43 pub usage: BTreeMap<String, i64>,
44 pub cost_usd: f64,
45 pub duration_ms: i64,
46 pub unknown_json_lines: Vec<String>,
47}
48
49fn new_output(schema_name: &str) -> ParsedJsonOutput {
50 ParsedJsonOutput {
51 schema_name: schema_name.into(),
52 events: Vec::new(),
53 final_text: String::new(),
54 session_id: String::new(),
55 error: String::new(),
56 usage: BTreeMap::new(),
57 cost_usd: 0.0,
58 duration_ms: 0,
59 unknown_json_lines: Vec::new(),
60 }
61}
62
63fn parser_state(
64 result: &ParsedJsonOutput,
65) -> (
66 usize,
67 String,
68 String,
69 String,
70 BTreeMap<String, i64>,
71 i64,
72 u64,
73) {
74 (
75 result.events.len(),
76 result.final_text.clone(),
77 result.error.clone(),
78 result.session_id.clone(),
79 result.usage.clone(),
80 result.duration_ms,
81 result.cost_usd.to_bits(),
82 )
83}
84
85fn parse_json_line(line: &str) -> Option<serde_json::Value> {
86 let trimmed = line.trim();
87 if trimmed.is_empty() {
88 return None;
89 }
90 let value: serde_json::Value = serde_json::from_str(trimmed).ok()?;
91 if value.is_object() {
92 Some(value)
93 } else {
94 None
95 }
96}
97
98fn apply_opencode_obj(result: &mut ParsedJsonOutput, obj: &serde_json::Value) {
99 if let Some(text) = obj.get("response").and_then(|value| value.as_str()) {
100 result.final_text = text.to_string();
101 result.events.push(JsonEvent {
102 event_type: "text".into(),
103 text: text.into(),
104 thinking: String::new(),
105 tool_call: None,
106 tool_result: None,
107 });
108 } else if let Some(err) = obj.get("error").and_then(|value| value.as_str()) {
109 result.error = err.to_string();
110 result.events.push(JsonEvent {
111 event_type: "error".into(),
112 text: err.into(),
113 thinking: String::new(),
114 tool_call: None,
115 tool_result: None,
116 });
117 } else if obj.get("type").and_then(|value| value.as_str()) == Some("reasoning") {
118 if let Some(session_id) = obj.get("sessionID").and_then(|value| value.as_str()) {
119 if !session_id.is_empty() {
120 result.session_id = session_id.to_string();
121 }
122 }
123 if let Some(part) = obj.get("part").and_then(|value| value.as_object()) {
124 if result.session_id.is_empty() {
125 if let Some(part_session_id) = part.get("sessionID").and_then(|value| value.as_str()) {
126 if !part_session_id.is_empty() {
127 result.session_id = part_session_id.to_string();
128 }
129 }
130 }
131 if let Some(text) = part.get("text").and_then(|value| value.as_str()) {
132 if !text.is_empty() {
133 result.events.push(JsonEvent {
134 event_type: "thinking".into(),
135 text: String::new(),
136 thinking: text.to_string(),
137 tool_call: None,
138 tool_result: None,
139 });
140 }
141 }
142 }
143 } else if obj.get("type").and_then(|value| value.as_str()) == Some("step_start") {
144 result.session_id = obj
145 .get("sessionID")
146 .and_then(|value| value.as_str())
147 .unwrap_or(&result.session_id)
148 .to_string();
149 } else if obj.get("type").and_then(|value| value.as_str()) == Some("text") {
150 if let Some(part) = obj.get("part").and_then(|value| value.as_object()) {
151 if let Some(text) = part.get("text").and_then(|value| value.as_str()) {
152 if !text.is_empty() {
153 result.final_text = text.to_string();
154 result.events.push(JsonEvent {
155 event_type: "text".into(),
156 text: text.into(),
157 thinking: String::new(),
158 tool_call: None,
159 tool_result: None,
160 });
161 }
162 }
163 }
164 } else if obj.get("type").and_then(|value| value.as_str()) == Some("tool_use") {
165 if let Some(part) = obj.get("part").and_then(|value| value.as_object()) {
166 let tool_name = part
167 .get("tool")
168 .and_then(|value| value.as_str())
169 .unwrap_or("")
170 .to_string();
171 let call_id = part
172 .get("callID")
173 .and_then(|value| value.as_str())
174 .unwrap_or("")
175 .to_string();
176 let state = part
177 .get("state")
178 .and_then(|value| value.as_object())
179 .cloned()
180 .unwrap_or_default();
181 let tool_input = state
182 .get("input")
183 .cloned()
184 .unwrap_or(serde_json::Value::Null);
185 let tool_output = state
186 .get("output")
187 .and_then(|value| value.as_str())
188 .unwrap_or("")
189 .to_string();
190 let is_error = state
191 .get("status")
192 .and_then(|value| value.as_str())
193 .map(|value| value.eq_ignore_ascii_case("error"))
194 .unwrap_or(false);
195 result.events.push(JsonEvent {
196 event_type: "tool_use".into(),
197 text: String::new(),
198 thinking: String::new(),
199 tool_call: Some(ToolCall {
200 id: call_id.clone(),
201 name: tool_name,
202 arguments: serde_json::to_string(&tool_input).unwrap_or_default(),
203 }),
204 tool_result: None,
205 });
206 result.events.push(JsonEvent {
207 event_type: "tool_result".into(),
208 text: String::new(),
209 thinking: String::new(),
210 tool_call: None,
211 tool_result: Some(ToolResult {
212 tool_call_id: call_id,
213 content: tool_output,
214 is_error,
215 }),
216 });
217 }
218 } else if obj.get("type").and_then(|value| value.as_str()) == Some("step_finish") {
219 if let Some(part) = obj.get("part").and_then(|value| value.as_object()) {
220 if let Some(tokens) = part.get("tokens").and_then(|value| value.as_object()) {
221 let mut usage = BTreeMap::new();
222 for key in ["total", "input", "output", "reasoning"] {
223 if let Some(value) = tokens.get(key).and_then(|value| value.as_i64()) {
224 usage.insert(key.to_string(), value);
225 }
226 }
227 if let Some(cache) = tokens.get("cache").and_then(|value| value.as_object()) {
228 for key in ["write", "read"] {
229 if let Some(value) = cache.get(key).and_then(|value| value.as_i64()) {
230 usage.insert(format!("cache_{key}"), value);
231 }
232 }
233 }
234 if !usage.is_empty() {
235 result.usage = usage;
236 }
237 }
238 if let Some(cost) = part.get("cost").and_then(|value| value.as_f64()) {
239 result.cost_usd = cost;
240 }
241 }
242 }
243}
244
245fn apply_claude_obj(result: &mut ParsedJsonOutput, obj: &serde_json::Value) -> bool {
246 let msg_type = obj
247 .get("type")
248 .and_then(|value| value.as_str())
249 .unwrap_or("");
250 match msg_type {
251 "system" => {
252 let subtype = obj
253 .get("subtype")
254 .and_then(|value| value.as_str())
255 .unwrap_or("");
256 if subtype == "init" {
257 result.session_id = obj
258 .get("session_id")
259 .and_then(|value| value.as_str())
260 .unwrap_or("")
261 .to_string();
262 return true;
263 } else if subtype == "api_retry" {
264 result.events.push(JsonEvent {
265 event_type: "system_retry".into(),
266 text: String::new(),
267 thinking: String::new(),
268 tool_call: None,
269 tool_result: None,
270 });
271 return true;
272 } else if matches!(
273 subtype,
274 "hook_started"
275 | "hook_progress"
276 | "hook_response"
277 | "status"
278 | "compact_boundary"
279 | "post_turn_summary"
280 | "local_command_output"
281 | "files_persisted"
282 | "task_notification"
283 | "task_started"
284 | "task_progress"
285 | "session_state_changed"
286 | "elicitation_complete"
287 | "bridge_state"
288 ) {
289 return true;
290 }
291 false
292 }
293 "assistant" => {
294 if let Some(message) = obj.get("message").and_then(|value| value.as_object()) {
295 if let Some(content) = message.get("content").and_then(|value| value.as_array()) {
296 let texts: Vec<String> = content
297 .iter()
298 .filter(|block| {
299 block.get("type").and_then(|value| value.as_str()) == Some("text")
300 })
301 .filter_map(|block| block.get("text").and_then(|value| value.as_str()))
302 .map(|text| text.to_string())
303 .collect();
304 if !texts.is_empty() {
305 result.final_text = texts.join("\n");
306 result.events.push(JsonEvent {
307 event_type: "assistant".into(),
308 text: result.final_text.clone(),
309 thinking: String::new(),
310 tool_call: None,
311 tool_result: None,
312 });
313 }
314 }
315 if let Some(usage) = message.get("usage").and_then(|value| value.as_object()) {
316 result.usage = usage
317 .iter()
318 .filter_map(|(key, value)| value.as_i64().map(|count| (key.clone(), count)))
319 .collect();
320 }
321 }
322 true
323 }
324 "user" => {
325 if let Some(message) = obj.get("message").and_then(|value| value.as_object()) {
326 if let Some(content) = message.get("content").and_then(|value| value.as_array()) {
327 for block in content {
328 if block.get("type").and_then(|value| value.as_str()) == Some("tool_result")
329 {
330 result.events.push(JsonEvent {
331 event_type: "tool_result".into(),
332 text: String::new(),
333 thinking: String::new(),
334 tool_call: None,
335 tool_result: Some(ToolResult {
336 tool_call_id: block
337 .get("tool_use_id")
338 .and_then(|value| value.as_str())
339 .unwrap_or("")
340 .to_string(),
341 content: block
342 .get("content")
343 .and_then(|value| value.as_str())
344 .unwrap_or("")
345 .to_string(),
346 is_error: block
347 .get("is_error")
348 .and_then(|value| value.as_bool())
349 .unwrap_or(false),
350 }),
351 });
352 }
353 }
354 }
355 }
356 true
357 }
358 "stream_event" => {
359 if let Some(event) = obj.get("event").and_then(|value| value.as_object()) {
360 let event_type = event
361 .get("type")
362 .and_then(|value| value.as_str())
363 .unwrap_or("");
364 if event_type == "content_block_delta" {
365 if let Some(delta) = event.get("delta").and_then(|value| value.as_object()) {
366 let delta_type = delta
367 .get("type")
368 .and_then(|value| value.as_str())
369 .unwrap_or("");
370 match delta_type {
371 "text_delta" => result.events.push(JsonEvent {
372 event_type: "text_delta".into(),
373 text: delta
374 .get("text")
375 .and_then(|value| value.as_str())
376 .unwrap_or("")
377 .to_string(),
378 thinking: String::new(),
379 tool_call: None,
380 tool_result: None,
381 }),
382 "thinking_delta" => result.events.push(JsonEvent {
383 event_type: "thinking_delta".into(),
384 text: String::new(),
385 thinking: delta
386 .get("thinking")
387 .and_then(|value| value.as_str())
388 .unwrap_or("")
389 .to_string(),
390 tool_call: None,
391 tool_result: None,
392 }),
393 "input_json_delta" => result.events.push(JsonEvent {
394 event_type: "tool_input_delta".into(),
395 text: delta
396 .get("partial_json")
397 .and_then(|value| value.as_str())
398 .unwrap_or("")
399 .to_string(),
400 thinking: String::new(),
401 tool_call: None,
402 tool_result: None,
403 }),
404 "signature_delta" | "citations_delta" | "connector_text_delta" => {}
405 _ => {}
406 }
407 }
408 } else if event_type == "content_block_start" {
409 if let Some(content_block) = event
410 .get("content_block")
411 .and_then(|value| value.as_object())
412 {
413 let block_type = content_block
414 .get("type")
415 .and_then(|value| value.as_str())
416 .unwrap_or("");
417 if block_type == "thinking" {
418 result.events.push(JsonEvent {
419 event_type: "thinking_start".into(),
420 text: String::new(),
421 thinking: String::new(),
422 tool_call: None,
423 tool_result: None,
424 });
425 } else if block_type == "tool_use" {
426 result.events.push(JsonEvent {
427 event_type: "tool_use_start".into(),
428 text: String::new(),
429 thinking: String::new(),
430 tool_call: Some(ToolCall {
431 id: content_block
432 .get("id")
433 .and_then(|value| value.as_str())
434 .unwrap_or("")
435 .to_string(),
436 name: content_block
437 .get("name")
438 .and_then(|value| value.as_str())
439 .unwrap_or("")
440 .to_string(),
441 arguments: String::new(),
442 }),
443 tool_result: None,
444 });
445 }
446 }
448 }
449 }
450 true
451 }
452 "tool_use" => {
453 let tool_input = obj
454 .get("tool_input")
455 .cloned()
456 .unwrap_or(serde_json::Value::Null);
457 result.events.push(JsonEvent {
458 event_type: "tool_use".into(),
459 text: String::new(),
460 thinking: String::new(),
461 tool_call: Some(ToolCall {
462 id: String::new(),
463 name: obj
464 .get("tool_name")
465 .and_then(|value| value.as_str())
466 .unwrap_or("")
467 .to_string(),
468 arguments: serde_json::to_string(&tool_input).unwrap_or_default(),
469 }),
470 tool_result: None,
471 });
472 true
473 }
474 "tool_result" => {
475 result.events.push(JsonEvent {
476 event_type: "tool_result".into(),
477 text: String::new(),
478 thinking: String::new(),
479 tool_call: None,
480 tool_result: Some(ToolResult {
481 tool_call_id: obj
482 .get("tool_use_id")
483 .and_then(|value| value.as_str())
484 .unwrap_or("")
485 .to_string(),
486 content: obj
487 .get("content")
488 .and_then(|value| value.as_str())
489 .unwrap_or("")
490 .to_string(),
491 is_error: obj
492 .get("is_error")
493 .and_then(|value| value.as_bool())
494 .unwrap_or(false),
495 }),
496 });
497 true
498 }
499 "result" => {
500 let subtype = obj
501 .get("subtype")
502 .and_then(|value| value.as_str())
503 .unwrap_or("");
504 if subtype == "success" {
505 result.final_text = obj
506 .get("result")
507 .and_then(|value| value.as_str())
508 .unwrap_or(&result.final_text)
509 .to_string();
510 result.cost_usd = obj
511 .get("cost_usd")
512 .and_then(|value| value.as_f64())
513 .unwrap_or(0.0);
514 result.duration_ms = obj
515 .get("duration_ms")
516 .and_then(|value| value.as_i64())
517 .unwrap_or(0);
518 if let Some(usage) = obj.get("usage").and_then(|value| value.as_object()) {
519 result.usage = usage
520 .iter()
521 .filter_map(|(key, value)| value.as_i64().map(|count| (key.clone(), count)))
522 .collect();
523 }
524 result.events.push(JsonEvent {
525 event_type: "result".into(),
526 text: result.final_text.clone(),
527 thinking: String::new(),
528 tool_call: None,
529 tool_result: None,
530 });
531 true
532 } else if matches!(
533 subtype,
534 "error"
535 | "error_during_execution"
536 | "error_max_turns"
537 | "error_max_budget_usd"
538 | "error_max_structured_output_retries"
539 ) {
540 result.error = obj
541 .get("error")
542 .and_then(|value| value.as_str())
543 .unwrap_or("")
544 .to_string();
545 result.events.push(JsonEvent {
546 event_type: "error".into(),
547 text: result.error.clone(),
548 thinking: String::new(),
549 tool_call: None,
550 tool_result: None,
551 });
552 true
553 } else {
554 false
555 }
556 }
557 "rate_limit_event"
558 | "tool_progress"
559 | "tool_use_summary"
560 | "auth_status"
561 | "streamlined_text"
562 | "streamlined_tool_use_summary"
563 | "prompt_suggestion" => true,
564 _ => false,
565 }
566}
567
568fn apply_kimi_obj(result: &mut ParsedJsonOutput, obj: &serde_json::Value) {
569 let passthrough_events = [
570 "TurnBegin",
571 "StepBegin",
572 "StepInterrupted",
573 "TurnEnd",
574 "StatusUpdate",
575 "HookTriggered",
576 "HookResolved",
577 "ApprovalRequest",
578 "SubagentEvent",
579 "ToolCallRequest",
580 ];
581 let wire_type = obj
582 .get("type")
583 .and_then(|value| value.as_str())
584 .unwrap_or("");
585 if passthrough_events.contains(&wire_type) {
586 result.events.push(JsonEvent {
587 event_type: wire_type.to_ascii_lowercase(),
588 text: String::new(),
589 thinking: String::new(),
590 tool_call: None,
591 tool_result: None,
592 });
593 return;
594 }
595
596 let role = obj
597 .get("role")
598 .and_then(|value| value.as_str())
599 .unwrap_or("");
600 if role == "assistant" {
601 if let Some(text) = obj.get("content").and_then(|value| value.as_str()) {
602 result.final_text = text.to_string();
603 result.events.push(JsonEvent {
604 event_type: "assistant".into(),
605 text: text.to_string(),
606 thinking: String::new(),
607 tool_call: None,
608 tool_result: None,
609 });
610 } else if let Some(parts) = obj.get("content").and_then(|value| value.as_array()) {
611 let mut texts = Vec::new();
612 for part in parts {
613 let part_type = part
614 .get("type")
615 .and_then(|value| value.as_str())
616 .unwrap_or("");
617 if part_type == "text" {
618 if let Some(text) = part.get("text").and_then(|value| value.as_str()) {
619 texts.push(text.to_string());
620 }
621 } else if part_type == "think" {
622 result.events.push(JsonEvent {
623 event_type: "thinking".into(),
624 text: String::new(),
625 thinking: part
626 .get("think")
627 .and_then(|value| value.as_str())
628 .unwrap_or("")
629 .to_string(),
630 tool_call: None,
631 tool_result: None,
632 });
633 }
634 }
635 if !texts.is_empty() {
636 result.final_text = texts.join("\n");
637 result.events.push(JsonEvent {
638 event_type: "assistant".into(),
639 text: result.final_text.clone(),
640 thinking: String::new(),
641 tool_call: None,
642 tool_result: None,
643 });
644 }
645 }
646 if let Some(tool_calls) = obj.get("tool_calls").and_then(|value| value.as_array()) {
647 for tool_call in tool_calls {
648 let function = tool_call
649 .get("function")
650 .and_then(|value| value.as_object());
651 result.events.push(JsonEvent {
652 event_type: "tool_call".into(),
653 text: String::new(),
654 thinking: String::new(),
655 tool_call: Some(ToolCall {
656 id: tool_call
657 .get("id")
658 .and_then(|value| value.as_str())
659 .unwrap_or("")
660 .to_string(),
661 name: function
662 .and_then(|f| f.get("name"))
663 .and_then(|value| value.as_str())
664 .unwrap_or("")
665 .to_string(),
666 arguments: function
667 .and_then(|f| f.get("arguments"))
668 .and_then(|value| value.as_str())
669 .unwrap_or("")
670 .to_string(),
671 }),
672 tool_result: None,
673 });
674 }
675 }
676 } else if role == "tool" {
677 let mut texts = Vec::new();
678 if let Some(parts) = obj.get("content").and_then(|value| value.as_array()) {
679 for part in parts {
680 if part.get("type").and_then(|value| value.as_str()) == Some("text") {
681 if let Some(text) = part.get("text").and_then(|value| value.as_str()) {
682 if !text.starts_with("<system>") {
683 texts.push(text.to_string());
684 }
685 }
686 }
687 }
688 }
689 result.events.push(JsonEvent {
690 event_type: "tool_result".into(),
691 text: String::new(),
692 thinking: String::new(),
693 tool_call: None,
694 tool_result: Some(ToolResult {
695 tool_call_id: obj
696 .get("tool_call_id")
697 .and_then(|value| value.as_str())
698 .unwrap_or("")
699 .to_string(),
700 content: texts.join("\n"),
701 is_error: false,
702 }),
703 });
704 }
705}
706
707fn message_text(message: &serde_json::Value) -> String {
708 if let Some(text) = message.get("content").and_then(|value| value.as_str()) {
709 return text.to_string();
710 }
711 let Some(content) = message.get("content").and_then(|value| value.as_array()) else {
712 return String::new();
713 };
714 content
715 .iter()
716 .filter(|block| block.get("type").and_then(|value| value.as_str()) == Some("text"))
717 .filter_map(|block| block.get("text").and_then(|value| value.as_str()))
718 .map(str::to_string)
719 .collect::<Vec<_>>()
720 .join("\n")
721}
722
723fn normalize_cursor_text(text: &str) -> String {
724 text.trim_matches('\n').to_string()
725}
726
727fn apply_cursor_agent_obj(result: &mut ParsedJsonOutput, obj: &serde_json::Value) {
728 match obj
729 .get("type")
730 .and_then(|value| value.as_str())
731 .unwrap_or("")
732 {
733 "system" => {
734 if obj.get("subtype").and_then(|value| value.as_str()) == Some("init") {
735 result.session_id = obj
736 .get("session_id")
737 .and_then(|value| value.as_str())
738 .unwrap_or("")
739 .to_string();
740 }
741 }
742 "assistant" => {
743 let text =
744 normalize_cursor_text(&obj.get("message").map(message_text).unwrap_or_default());
745 if !text.is_empty() {
746 result.final_text = text.clone();
747 result.events.push(JsonEvent {
748 event_type: "assistant".into(),
749 text,
750 thinking: String::new(),
751 tool_call: None,
752 tool_result: None,
753 });
754 }
755 }
756 "result" => {
757 if let Some(session_id) = obj.get("session_id").and_then(|value| value.as_str()) {
758 result.session_id = session_id.to_string();
759 }
760 if let Some(duration) = obj.get("duration_ms").and_then(|value| value.as_i64()) {
761 result.duration_ms = duration;
762 }
763 if let Some(usage) = obj.get("usage").and_then(|value| value.as_object()) {
764 result.usage = usage
765 .iter()
766 .filter_map(|(key, value)| value.as_i64().map(|number| (key.clone(), number)))
767 .collect();
768 }
769 let is_error = obj
770 .get("is_error")
771 .and_then(|value| value.as_bool())
772 .unwrap_or(false);
773 let subtype = obj
774 .get("subtype")
775 .and_then(|value| value.as_str())
776 .unwrap_or("");
777 if subtype == "success" && !is_error {
778 let text = normalize_cursor_text(
779 obj.get("result")
780 .and_then(|value| value.as_str())
781 .unwrap_or(&result.final_text),
782 );
783 result.final_text = text.clone();
784 if !text.is_empty() {
785 result.events.push(JsonEvent {
786 event_type: "result".into(),
787 text,
788 thinking: String::new(),
789 tool_call: None,
790 tool_result: None,
791 });
792 }
793 } else {
794 let text = obj
795 .get("error")
796 .or_else(|| obj.get("result"))
797 .and_then(|value| value.as_str())
798 .unwrap_or("")
799 .to_string();
800 result.error = text.clone();
801 result.events.push(JsonEvent {
802 event_type: "error".into(),
803 text,
804 thinking: String::new(),
805 tool_call: None,
806 tool_result: None,
807 });
808 }
809 }
810 _ => {}
811 }
812}
813
814fn apply_codex_obj(result: &mut ParsedJsonOutput, obj: &serde_json::Value) -> bool {
815 match obj
816 .get("type")
817 .and_then(|value| value.as_str())
818 .unwrap_or("")
819 {
820 "thread.started" => {
821 result.session_id = obj
822 .get("thread_id")
823 .and_then(|value| value.as_str())
824 .unwrap_or("")
825 .to_string();
826 true
827 }
828 "turn.started" => true,
829 "turn.completed" => {
830 if let Some(usage) = obj.get("usage").and_then(|value| value.as_object()) {
831 result.usage = usage
832 .iter()
833 .filter_map(|(key, value)| value.as_i64().map(|count| (key.clone(), count)))
834 .collect();
835 }
836 true
837 }
838 "error" => {
839 let text = obj
840 .get("message")
841 .or_else(|| obj.get("error"))
842 .map(codex_error_event_text)
843 .unwrap_or_default();
844 record_codex_error(result, text)
845 }
846 "turn.failed" => {
847 let text = obj
848 .get("error")
849 .map(codex_error_event_text)
850 .unwrap_or_default();
851 record_codex_error(result, text)
852 }
853 "item.started" | "item.completed" => {
854 let Some(item) = obj.get("item").and_then(|value| value.as_object()) else {
855 return false;
856 };
857 let item_type = item
858 .get("type")
859 .and_then(|value| value.as_str())
860 .unwrap_or("");
861 if item_type == "agent_message"
862 && obj.get("type").and_then(|value| value.as_str()) == Some("item.completed")
863 {
864 let text = item
865 .get("text")
866 .and_then(|value| value.as_str())
867 .unwrap_or("")
868 .to_string();
869 result.final_text = text.clone();
870 result.events.push(JsonEvent {
871 event_type: "assistant".into(),
872 text,
873 thinking: String::new(),
874 tool_call: None,
875 tool_result: None,
876 });
877 true
878 } else if item_type == "command_execution" {
879 let call_id = item
880 .get("id")
881 .and_then(|value| value.as_str())
882 .unwrap_or("")
883 .to_string();
884 let command = item
885 .get("command")
886 .and_then(|value| value.as_str())
887 .unwrap_or("")
888 .to_string();
889 if obj.get("type").and_then(|value| value.as_str()) == Some("item.started") {
890 result.events.push(JsonEvent {
891 event_type: "tool_use_start".into(),
892 text: String::new(),
893 thinking: String::new(),
894 tool_call: Some(ToolCall {
895 id: call_id,
896 name: "command_execution".into(),
897 arguments: serde_json::json!({ "command": command }).to_string(),
898 }),
899 tool_result: None,
900 });
901 true
902 } else {
903 let status = item
904 .get("status")
905 .and_then(|value| value.as_str())
906 .unwrap_or("");
907 let exit_code = item.get("exit_code").and_then(|value| value.as_i64());
908 result.events.push(JsonEvent {
909 event_type: "tool_result".into(),
910 text: String::new(),
911 thinking: String::new(),
912 tool_call: None,
913 tool_result: Some(ToolResult {
914 tool_call_id: call_id,
915 content: item
916 .get("aggregated_output")
917 .and_then(|value| value.as_str())
918 .unwrap_or("")
919 .to_string(),
920 is_error: exit_code.is_some_and(|code| code != 0)
921 || (!status.is_empty() && status != "completed"),
922 }),
923 });
924 true
925 }
926 } else {
927 false
928 }
929 }
930 _ => false,
931 }
932}
933
934fn record_codex_error(result: &mut ParsedJsonOutput, text: String) -> bool {
935 if text.is_empty() {
936 return false;
937 }
938 if result.error == text {
939 return true;
940 }
941 result.error = text.clone();
942 result.events.push(JsonEvent {
943 event_type: "error".into(),
944 text,
945 thinking: String::new(),
946 tool_call: None,
947 tool_result: None,
948 });
949 true
950}
951
952fn codex_error_event_text(value: &serde_json::Value) -> String {
953 if let Some(obj) = value.as_object() {
954 if let Some(message) = obj.get("message") {
955 let nested = codex_error_event_text(message);
956 if !nested.is_empty() {
957 return nested;
958 }
959 }
960 return format_codex_error_payload(value);
961 }
962
963 let Some(text) = value.as_str().map(str::trim) else {
964 return String::new();
965 };
966 if text.is_empty() {
967 return String::new();
968 }
969 let Ok(decoded) = serde_json::from_str::<serde_json::Value>(text) else {
970 return text.to_string();
971 };
972 if decoded.as_object().is_some() {
973 let formatted = format_codex_error_payload(&decoded);
974 if !formatted.is_empty() {
975 return formatted;
976 }
977 }
978 text.to_string()
979}
980
981fn format_codex_error_payload(payload: &serde_json::Value) -> String {
982 let error = payload.get("error");
983 let error_obj = error.and_then(|value| value.as_object());
984 let message = error_obj
985 .and_then(|obj| obj.get("message"))
986 .or_else(|| payload.get("message"))
987 .or(error)
988 .and_then(|value| value.as_str())
989 .unwrap_or("");
990 if message.is_empty() {
991 return String::new();
992 }
993
994 let status = payload.get("status").and_then(|value| value.as_i64());
995 let error_type = error_obj
996 .and_then(|obj| obj.get("type"))
997 .or_else(|| payload.get("type"))
998 .and_then(|value| value.as_str())
999 .unwrap_or("");
1000 let mut prefix = String::new();
1001 if !error_type.is_empty() && error_type != "error" {
1002 prefix.push_str(error_type);
1003 }
1004 if let Some(status) = status {
1005 if prefix.is_empty() {
1006 prefix = format!("HTTP {status}");
1007 } else {
1008 prefix = format!("{prefix} ({status})");
1009 }
1010 }
1011 if prefix.is_empty() {
1012 message.to_string()
1013 } else {
1014 format!("{prefix}: {message}")
1015 }
1016}
1017
1018fn apply_gemini_stats(
1019 result: &mut ParsedJsonOutput,
1020 stats: &serde_json::Map<String, serde_json::Value>,
1021) {
1022 let usage: BTreeMap<String, i64> = stats
1023 .iter()
1024 .filter_map(|(key, value)| value.as_i64().map(|count| (key.clone(), count)))
1025 .collect();
1026 if !usage.is_empty() {
1027 result.usage = usage;
1028 }
1029 if let Some(duration_ms) = stats.get("duration_ms").and_then(|value| value.as_i64()) {
1030 result.duration_ms = duration_ms;
1031 }
1032}
1033
1034fn apply_gemini_obj(result: &mut ParsedJsonOutput, obj: &serde_json::Value) -> bool {
1035 if let Some(session_id) = obj.get("session_id").and_then(|value| value.as_str()) {
1036 if !session_id.is_empty() {
1037 result.session_id = session_id.to_string();
1038 }
1039 }
1040
1041 if let Some(response) = obj.get("response").and_then(|value| value.as_str()) {
1042 result.final_text = response.to_string();
1043 if !response.is_empty() {
1044 result.events.push(JsonEvent {
1045 event_type: "assistant".into(),
1046 text: response.into(),
1047 thinking: String::new(),
1048 tool_call: None,
1049 tool_result: None,
1050 });
1051 }
1052 if let Some(stats) = obj.get("stats").and_then(|value| value.as_object()) {
1053 apply_gemini_stats(result, stats);
1054 }
1055 return true;
1056 }
1057
1058 match obj
1059 .get("type")
1060 .and_then(|value| value.as_str())
1061 .unwrap_or("")
1062 {
1063 "init" => true,
1064 "message" => {
1065 let role = obj
1066 .get("role")
1067 .and_then(|value| value.as_str())
1068 .unwrap_or("");
1069 if role == "assistant" {
1070 let text = obj
1071 .get("content")
1072 .and_then(|value| value.as_str())
1073 .unwrap_or("");
1074 result.final_text.push_str(text);
1075 if !text.is_empty() {
1076 result.events.push(JsonEvent {
1077 event_type: if obj
1078 .get("delta")
1079 .and_then(|value| value.as_bool())
1080 .unwrap_or(false)
1081 {
1082 "text_delta".into()
1083 } else {
1084 "assistant".into()
1085 },
1086 text: text.into(),
1087 thinking: String::new(),
1088 tool_call: None,
1089 tool_result: None,
1090 });
1091 }
1092 true
1093 } else {
1094 role == "user"
1095 }
1096 }
1097 "result" => {
1098 if let Some(stats) = obj.get("stats").and_then(|value| value.as_object()) {
1099 apply_gemini_stats(result, stats);
1100 }
1101 let status = obj
1102 .get("status")
1103 .and_then(|value| value.as_str())
1104 .unwrap_or("");
1105 if !status.is_empty() && status != "success" {
1106 result.error = obj
1107 .get("error")
1108 .and_then(|value| value.as_str())
1109 .unwrap_or(status)
1110 .to_string();
1111 result.events.push(JsonEvent {
1112 event_type: "error".into(),
1113 text: result.error.clone(),
1114 thinking: String::new(),
1115 tool_call: None,
1116 tool_result: None,
1117 });
1118 }
1119 true
1120 }
1121 _ => false,
1122 }
1123}
1124
1125pub fn parse_opencode_json(raw: &str) -> ParsedJsonOutput {
1126 let mut result = new_output("opencode");
1127 for line in raw.lines() {
1128 if let Some(obj) = parse_json_line(line) {
1129 let is_reasoning = obj.get("type").and_then(|value| value.as_str()) == Some("reasoning");
1130 let before = parser_state(&result);
1131 apply_opencode_obj(&mut result, &obj);
1132 let after = parser_state(&result);
1133 if before == after && !is_reasoning {
1134 result.unknown_json_lines.push(line.trim().to_string());
1135 }
1136 }
1137 }
1138 result
1139}
1140
1141pub fn parse_claude_code_json(raw: &str) -> ParsedJsonOutput {
1142 let mut result = new_output("claude-code");
1143 for line in raw.lines() {
1144 if let Some(obj) = parse_json_line(line) {
1145 if !apply_claude_obj(&mut result, &obj) {
1146 result.unknown_json_lines.push(line.trim().to_string());
1147 }
1148 }
1149 }
1150 result
1151}
1152
1153pub fn parse_kimi_json(raw: &str) -> ParsedJsonOutput {
1154 let mut result = new_output("kimi");
1155 for line in raw.lines() {
1156 if let Some(obj) = parse_json_line(line) {
1157 let before = parser_state(&result);
1158 apply_kimi_obj(&mut result, &obj);
1159 let after = parser_state(&result);
1160 if before == after {
1161 result.unknown_json_lines.push(line.trim().to_string());
1162 }
1163 }
1164 }
1165 result
1166}
1167
1168pub fn parse_cursor_agent_json(raw: &str) -> ParsedJsonOutput {
1169 let mut result = new_output("cursor-agent");
1170 for line in raw.lines() {
1171 if let Some(obj) = parse_json_line(line) {
1172 let before = parser_state(&result);
1173 apply_cursor_agent_obj(&mut result, &obj);
1174 let after = parser_state(&result);
1175 if before == after {
1176 result.unknown_json_lines.push(line.trim().to_string());
1177 }
1178 }
1179 }
1180 result
1181}
1182
1183pub fn parse_codex_json(raw: &str) -> ParsedJsonOutput {
1184 let mut result = new_output("codex");
1185 for line in raw.lines() {
1186 if let Some(obj) = parse_json_line(line) {
1187 if !apply_codex_obj(&mut result, &obj) {
1188 result.unknown_json_lines.push(line.trim().to_string());
1189 }
1190 }
1191 }
1192 result
1193}
1194
1195pub fn parse_gemini_json(raw: &str) -> ParsedJsonOutput {
1196 let mut result = new_output("gemini");
1197 for line in raw.lines() {
1198 if let Some(obj) = parse_json_line(line) {
1199 if !apply_gemini_obj(&mut result, &obj) {
1200 result.unknown_json_lines.push(line.trim().to_string());
1201 }
1202 }
1203 }
1204 result
1205}
1206
1207pub fn parse_json_output(raw: &str, schema: &str) -> ParsedJsonOutput {
1208 match schema {
1209 "opencode" => parse_opencode_json(raw),
1210 "claude-code" => parse_claude_code_json(raw),
1211 "kimi" => parse_kimi_json(raw),
1212 "cursor-agent" => parse_cursor_agent_json(raw),
1213 "codex" => parse_codex_json(raw),
1214 "gemini" => parse_gemini_json(raw),
1215 _ => ParsedJsonOutput {
1216 schema_name: schema.into(),
1217 events: Vec::new(),
1218 final_text: String::new(),
1219 session_id: String::new(),
1220 error: format!("unknown schema: {schema}"),
1221 usage: BTreeMap::new(),
1222 cost_usd: 0.0,
1223 duration_ms: 0,
1224 unknown_json_lines: Vec::new(),
1225 },
1226 }
1227}
1228
1229fn truncate_to_char_limit(text: &str, max_chars: usize) -> Option<String> {
1230 text.char_indices()
1231 .nth(max_chars)
1232 .map(|(index, _)| text[..index].to_string())
1233}
1234
1235fn summarize_text(text: &str, max_lines: usize, max_chars: usize) -> String {
1236 let lines: Vec<&str> = text.trim().lines().collect();
1237 if lines.is_empty() {
1238 return String::new();
1239 }
1240 let mut clipped = lines
1241 .into_iter()
1242 .take(max_lines)
1243 .collect::<Vec<_>>()
1244 .join("\n");
1245 let mut truncated = text.trim().lines().count() > max_lines;
1246 if let Some(safe_clipped) = truncate_to_char_limit(&clipped, max_chars) {
1247 clipped = safe_clipped;
1248 clipped = clipped.trim_end().to_string();
1249 truncated = true;
1250 }
1251 if truncated {
1252 clipped.push_str(" …");
1253 }
1254 clipped
1255}
1256
1257fn parse_tool_arguments(arguments: &str) -> Option<serde_json::Map<String, serde_json::Value>> {
1258 let value: serde_json::Value = serde_json::from_str(arguments).ok()?;
1259 value.as_object().cloned()
1260}
1261
1262fn bash_command_preview(tool_call: &ToolCall) -> Option<String> {
1263 let args = parse_tool_arguments(&tool_call.arguments)?;
1264 for key in ["command", "cmd", "bash_command", "script"] {
1265 if let Some(value) = args.get(key).and_then(|value| value.as_str()) {
1266 let mut preview = value.trim().to_string();
1267 if preview.is_empty() {
1268 continue;
1269 }
1270 if let Some(safe_preview) = truncate_to_char_limit(&preview, 400) {
1271 preview = safe_preview.trim_end().to_string() + " …";
1272 }
1273 return Some(preview);
1274 }
1275 }
1276 None
1277}
1278
1279fn tool_preview(tool_name: &str, text: &str) -> String {
1280 match tool_name.to_ascii_lowercase().as_str() {
1281 "read" | "write" | "edit" | "multiedit" | "read_file" | "write_file" | "edit_file" => {
1282 String::new()
1283 }
1284 _ => summarize_text(text, 8, 400),
1285 }
1286}
1287
1288pub fn resolve_human_tty(tty: bool, force_color: Option<&str>, no_color: Option<&str>) -> bool {
1289 if force_color.is_some_and(|value| !value.is_empty()) {
1290 return true;
1291 }
1292 if no_color.is_some_and(|value| !value.is_empty()) {
1293 return false;
1294 }
1295 tty
1296}
1297
1298fn style(text: &str, code: &str, tty: bool) -> String {
1299 if tty {
1300 format!("\x1b[{code}m{text}\x1b[0m")
1301 } else {
1302 text.to_string()
1303 }
1304}
1305
1306pub struct FormattedRenderer {
1307 show_thinking: bool,
1308 tty: bool,
1309 seen_final_texts: BTreeSet<String>,
1310 tool_calls_by_id: BTreeMap<String, ToolCall>,
1311 pending_tool_call: Option<ToolCall>,
1312 streamed_assistant_buffer: String,
1313 plain_text_tool_work: bool,
1314}
1315
1316impl FormattedRenderer {
1317 pub fn new(show_thinking: bool, tty: bool) -> Self {
1318 Self {
1319 show_thinking,
1320 tty,
1321 seen_final_texts: BTreeSet::new(),
1322 tool_calls_by_id: BTreeMap::new(),
1323 pending_tool_call: None,
1324 streamed_assistant_buffer: String::new(),
1325 plain_text_tool_work: false,
1326 }
1327 }
1328
1329 pub fn render_output(&mut self, output: &ParsedJsonOutput) -> String {
1330 output
1331 .events
1332 .iter()
1333 .filter_map(|event| self.render_event(event))
1334 .collect::<Vec<_>>()
1335 .join("\n")
1336 }
1337
1338 pub fn render_event(&mut self, event: &JsonEvent) -> Option<String> {
1339 match event.event_type.as_str() {
1340 "text_delta" if !event.text.is_empty() => {
1341 self.streamed_assistant_buffer.push_str(&event.text);
1342 Some(self.render_message("assistant", &event.text))
1343 }
1344 "text" | "assistant" if !event.text.is_empty() => {
1345 if !self.streamed_assistant_buffer.is_empty()
1346 && event.text == self.streamed_assistant_buffer
1347 {
1348 self.seen_final_texts.insert(event.text.clone());
1349 self.streamed_assistant_buffer.clear();
1350 None
1351 } else {
1352 self.streamed_assistant_buffer.clear();
1353 Some(self.render_message("assistant", &event.text))
1354 }
1355 }
1356 "result" if !event.text.is_empty() => {
1357 if !self.streamed_assistant_buffer.is_empty()
1358 && event.text == self.streamed_assistant_buffer
1359 {
1360 self.seen_final_texts.insert(event.text.clone());
1361 self.streamed_assistant_buffer.clear();
1362 None
1363 } else if self.seen_final_texts.contains(&event.text) {
1364 None
1365 } else {
1366 self.streamed_assistant_buffer.clear();
1367 Some(self.render_message("success", &event.text))
1368 }
1369 }
1370 "thinking" | "thinking_delta" if !event.thinking.is_empty() && self.show_thinking => {
1371 Some(self.render_message("thinking", &event.thinking))
1372 }
1373 "tool_use" | "tool_use_start" | "tool_call" => {
1374 if let Some(tool_call) = &event.tool_call {
1375 self.streamed_assistant_buffer.clear();
1376 if !tool_call.id.is_empty() {
1377 self.tool_calls_by_id
1378 .insert(tool_call.id.clone(), tool_call.clone());
1379 }
1380 self.pending_tool_call = Some(tool_call.clone());
1381 self.plain_text_tool_work = true;
1382 Some(self.render_tool_start(tool_call))
1383 } else {
1384 None
1385 }
1386 }
1387 "tool_input_delta" if !event.text.is_empty() => {
1388 if let Some(tool_call) = &mut self.pending_tool_call {
1389 tool_call.arguments.push_str(&event.text);
1390 if !tool_call.id.is_empty() {
1391 self.tool_calls_by_id
1392 .insert(tool_call.id.clone(), tool_call.clone());
1393 }
1394 }
1395 None
1396 }
1397 "tool_result" => event.tool_result.as_ref().map(|tool_result| {
1398 self.streamed_assistant_buffer.clear();
1399 self.render_tool_result(tool_result)
1400 }),
1401 "error" if !event.text.is_empty() => {
1402 self.streamed_assistant_buffer.clear();
1403 Some(self.render_message("error", &event.text))
1404 }
1405 _ => None,
1406 }
1407 }
1408
1409 fn render_message(&mut self, kind: &str, text: &str) -> String {
1410 if matches!(kind, "assistant" | "success") {
1411 self.seen_final_texts.insert(text.to_string());
1412 }
1413 let prefix = match kind {
1414 "assistant" => renderer_prefix(
1415 "💬",
1416 "[assistant]",
1417 "96",
1418 self.tty,
1419 self.plain_text_tool_work,
1420 ),
1421 "thinking" => renderer_prefix(
1422 "🧠",
1423 "[thinking]",
1424 "2;35",
1425 self.tty,
1426 self.plain_text_tool_work,
1427 ),
1428 "success" => renderer_prefix("✅", "[ok]", "92", self.tty, self.plain_text_tool_work),
1429 _ => renderer_prefix("❌", "[error]", "91", self.tty, self.plain_text_tool_work),
1430 };
1431 with_prefix(&prefix, text)
1432 }
1433
1434 fn render_tool_start(&self, tool_call: &ToolCall) -> String {
1435 let prefix = prefix("🛠️", "[tool:start]", "94", self.tty);
1436 let mut detail = tool_call.name.clone();
1437 if let Some(preview) = bash_command_preview(tool_call) {
1438 detail.push_str(": ");
1439 detail.push_str(&preview);
1440 }
1441 with_prefix(&prefix, &detail)
1442 }
1443
1444 fn render_tool_result(&self, tool_result: &ToolResult) -> String {
1445 let prefix = prefix("📎", "[tool:result]", "36", self.tty);
1446 let tool_call = self
1447 .tool_calls_by_id
1448 .get(&tool_result.tool_call_id)
1449 .or(self.pending_tool_call.as_ref());
1450 let tool_name = tool_call
1451 .map(|tool_call| tool_call.name.clone())
1452 .unwrap_or_else(|| "tool".into());
1453 let mut summary = format!(
1454 "{} ({})",
1455 tool_name,
1456 if tool_result.is_error { "error" } else { "ok" }
1457 );
1458 if let Some(tool_call) = tool_call {
1459 if let Some(preview) = bash_command_preview(tool_call) {
1460 summary.push_str(": ");
1461 summary.push_str(&preview);
1462 }
1463 }
1464 let preview = tool_preview(&tool_name, &tool_result.content);
1465 if !preview.is_empty() {
1466 summary.push('\n');
1467 summary.push_str(&preview);
1468 }
1469 with_prefix(&prefix, &summary)
1470 }
1471}
1472
1473fn prefix(emoji: &str, plain: &str, color_code: &str, tty: bool) -> String {
1474 if tty {
1475 style(emoji, color_code, true)
1476 } else {
1477 plain.to_string()
1478 }
1479}
1480
1481fn renderer_prefix(
1482 emoji: &str,
1483 plain: &str,
1484 color_code: &str,
1485 tty: bool,
1486 plain_text_tool_work: bool,
1487) -> String {
1488 if tty {
1489 return style(emoji, color_code, true);
1490 }
1491 if plain_text_tool_work && matches!(plain, "[assistant]" | "[thinking]" | "[ok]" | "[error]") {
1492 return plain.to_string();
1493 }
1494 plain.to_string()
1495}
1496
1497fn with_prefix(prefix: &str, text: &str) -> String {
1498 text.lines()
1499 .map(|line| {
1500 if line.is_empty() {
1501 prefix.to_string()
1502 } else {
1503 format!("{prefix} {line}")
1504 }
1505 })
1506 .collect::<Vec<_>>()
1507 .join("\n")
1508}
1509
1510pub struct StructuredStreamProcessor {
1511 schema: String,
1512 renderer: FormattedRenderer,
1513 output: ParsedJsonOutput,
1514 buffer: String,
1515 unknown_json_lines: Vec<String>,
1516}
1517
1518impl StructuredStreamProcessor {
1519 pub fn new(schema: &str, renderer: FormattedRenderer) -> Self {
1520 Self {
1521 schema: schema.into(),
1522 renderer,
1523 output: new_output(schema),
1524 buffer: String::new(),
1525 unknown_json_lines: Vec::new(),
1526 }
1527 }
1528
1529 pub fn output(&self) -> &ParsedJsonOutput {
1530 &self.output
1531 }
1532
1533 pub fn feed(&mut self, chunk: &str) -> String {
1534 self.buffer.push_str(chunk);
1535 let mut rendered = Vec::new();
1536 while let Some(index) = self.buffer.find('\n') {
1537 let line = self.buffer[..index].to_string();
1538 self.buffer = self.buffer[index + 1..].to_string();
1539 if let Some(obj) = parse_json_line(&line) {
1540 let before = parser_state(&self.output);
1541 let event_count = self.output.events.len();
1542 let recognized = self.apply(&obj);
1543 let after = parser_state(&self.output);
1544 if before == after && !recognized {
1545 self.unknown_json_lines.push(line.trim().to_string());
1546 }
1547 for event in &self.output.events[event_count..] {
1548 if let Some(text) = self.renderer.render_event(event) {
1549 rendered.push(text);
1550 }
1551 }
1552 }
1553 }
1554 rendered.join("\n")
1555 }
1556
1557 pub fn finish(&mut self) -> String {
1558 if self.buffer.trim().is_empty() {
1559 return String::new();
1560 }
1561 let line = std::mem::take(&mut self.buffer);
1562 if let Some(obj) = parse_json_line(&line) {
1563 let before = parser_state(&self.output);
1564 let event_count = self.output.events.len();
1565 let recognized = self.apply(&obj);
1566 let after = parser_state(&self.output);
1567 if before == after && !recognized {
1568 self.unknown_json_lines.push(line.trim().to_string());
1569 }
1570 return self.output.events[event_count..]
1571 .iter()
1572 .filter_map(|event| self.renderer.render_event(event))
1573 .collect::<Vec<_>>()
1574 .join("\n");
1575 }
1576 String::new()
1577 }
1578
1579 pub fn take_unknown_json_lines(&mut self) -> Vec<String> {
1580 std::mem::take(&mut self.unknown_json_lines)
1581 }
1582
1583 fn apply(&mut self, obj: &serde_json::Value) -> bool {
1584 match self.schema.as_str() {
1585 "opencode" => {
1586 apply_opencode_obj(&mut self.output, obj);
1587 false
1588 }
1589 "claude-code" => apply_claude_obj(&mut self.output, obj),
1590 "kimi" => {
1591 apply_kimi_obj(&mut self.output, obj);
1592 false
1593 }
1594 "cursor-agent" => {
1595 apply_cursor_agent_obj(&mut self.output, obj);
1596 false
1597 }
1598 "codex" => apply_codex_obj(&mut self.output, obj),
1599 "gemini" => apply_gemini_obj(&mut self.output, obj),
1600 _ => false,
1601 }
1602 }
1603}
1604
1605pub fn render_parsed(output: &ParsedJsonOutput, show_thinking: bool, tty: bool) -> String {
1606 let mut renderer = FormattedRenderer::new(show_thinking, tty);
1607 let rendered = renderer.render_output(output);
1608 if rendered.is_empty() {
1609 output.final_text.clone()
1610 } else {
1611 rendered
1612 }
1613}