Skip to main content

call_coding_clis/
json_output.rs

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                        // text, server_tool_use, connector_text, advisor_tool_result: silent
447                    }
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}