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
1207fn apply_pi_obj(result: &mut ParsedJsonOutput, obj: &serde_json::Value) -> bool {
1208    let msg_type = obj.get("type").and_then(|v| v.as_str()).unwrap_or("");
1209
1210    match msg_type {
1211        "session" => {
1212            if let Some(id) = obj.get("id").and_then(|v| v.as_str()) {
1213                if !id.is_empty() {
1214                    result.session_id = id.to_string();
1215                }
1216            }
1217            true
1218        }
1219        "agent_start" | "agent_end" | "turn_start" | "message_start" | "message_end" => true,
1220        "turn_end" => {
1221            if let Some(message) = obj.get("message").and_then(|v| v.as_object()) {
1222                if let Some(usage) = message.get("usage").and_then(|v| v.as_object()) {
1223                    let mut usage_map = BTreeMap::new();
1224                    for key in &["input", "output", "cacheRead", "cacheWrite", "totalTokens"] {
1225                        if let Some(val) = usage.get(*key).and_then(|v| v.as_i64()) {
1226                            usage_map.insert(key.to_string(), val as i64);
1227                        }
1228                    }
1229                    if !usage_map.is_empty() {
1230                        result.usage = usage_map;
1231                    }
1232                    if let Some(cost) = usage.get("cost").and_then(|v| v.as_object()) {
1233                        if let Some(total) = cost.get("total").and_then(|v| v.as_f64()) {
1234                            result.cost_usd = total;
1235                        }
1236                    }
1237                }
1238            }
1239            true
1240        }
1241        "message_update" => {
1242            if let Some(event) = obj.get("assistantMessageEvent").and_then(|v| v.as_object()) {
1243                let event_subtype = event.get("type").and_then(|v| v.as_str()).unwrap_or("");
1244                match event_subtype {
1245                    "text_end" => {
1246                        if let Some(content) = event.get("content").and_then(|v| v.as_str()) {
1247                            if !content.is_empty() {
1248                                result.final_text = content.to_string();
1249                                result.events.push(JsonEvent {
1250                                    event_type: "text".into(),
1251                                    text: content.to_string(),
1252                                    thinking: String::new(),
1253                                    tool_call: None,
1254                                    tool_result: None,
1255                                });
1256                            }
1257                        }
1258                        true
1259                    }
1260                    "thinking_end" => {
1261                        if let Some(content) = event.get("content").and_then(|v| v.as_str()) {
1262                            if !content.is_empty() {
1263                                result.events.push(JsonEvent {
1264                                    event_type: "thinking".into(),
1265                                    text: String::new(),
1266                                    thinking: content.to_string(),
1267                                    tool_call: None,
1268                                    tool_result: None,
1269                                });
1270                            }
1271                        }
1272                        true
1273                    }
1274                    "toolcall_end" => {
1275                        if let Some(tool_call_obj) = event.get("toolCall").and_then(|v| v.as_object()) {
1276                            let call_id = tool_call_obj.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string();
1277                            let tool_name = tool_call_obj.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
1278                            let arguments = tool_call_obj.get("arguments").map(|v| {
1279                                if v.is_object() {
1280                                    serde_json::to_string(v).unwrap_or_default()
1281                                } else {
1282                                    v.as_str().unwrap_or("").to_string()
1283                                }
1284                            }).unwrap_or_default();
1285                            result.events.push(JsonEvent {
1286                                event_type: "tool_use".into(),
1287                                text: String::new(),
1288                                thinking: String::new(),
1289                                tool_call: Some(ToolCall {
1290                                    id: call_id,
1291                                    name: tool_name,
1292                                    arguments,
1293                                }),
1294                                tool_result: None,
1295                            });
1296                        }
1297                        true
1298                    }
1299                    _ => true,
1300                }
1301            } else {
1302                true
1303            }
1304        }
1305        "tool_execution_end" => {
1306            let tool_call_id = obj.get("toolCallId").and_then(|v| v.as_str()).unwrap_or("").to_string();
1307            let is_error = obj.get("isError").and_then(|v| v.as_bool()).unwrap_or(false);
1308            let mut content = String::new();
1309            if let Some(tool_result) = obj.get("result").and_then(|v| v.as_object()) {
1310                if let Some(content_items) = tool_result.get("content").and_then(|v| v.as_array()) {
1311                    let parts: Vec<String> = content_items.iter()
1312                        .filter_map(|item| {
1313                            if item.get("type").and_then(|v| v.as_str()) == Some("text") {
1314                                item.get("text").and_then(|v| v.as_str()).map(|s| s.to_string())
1315                            } else {
1316                                None
1317                            }
1318                        })
1319                        .collect();
1320                    content = parts.join("\n");
1321                }
1322            }
1323            result.events.push(JsonEvent {
1324                event_type: "tool_result".into(),
1325                text: String::new(),
1326                thinking: String::new(),
1327                tool_call: None,
1328                tool_result: Some(ToolResult {
1329                    tool_call_id,
1330                    content,
1331                    is_error,
1332                }),
1333            });
1334            true
1335        }
1336        "tool_execution_start" | "tool_execution_update" => true,
1337        _ => true,
1338    }
1339}
1340
1341pub fn parse_pi_json(raw: &str) -> ParsedJsonOutput {
1342    let mut result = new_output("pi");
1343    for line in raw.lines() {
1344        if let Some(obj) = parse_json_line(line) {
1345            if !apply_pi_obj(&mut result, &obj) {
1346                result.unknown_json_lines.push(line.trim().to_string());
1347            }
1348        }
1349    }
1350    result
1351}
1352
1353fn apply_grok_obj(result: &mut ParsedJsonOutput, obj: &serde_json::Value) -> bool {
1354    let msg_type = obj.get("type").and_then(|v| v.as_str()).unwrap_or("");
1355
1356    // One-shot final object: {text, stopReason, sessionId, requestId, thought?}
1357    if msg_type.is_empty()
1358        && (obj.get("text").is_some()
1359            || obj.get("sessionId").is_some()
1360            || obj.get("stopReason").is_some())
1361    {
1362        if let Some(session_id) = obj.get("sessionId").and_then(|v| v.as_str()) {
1363            if !session_id.is_empty() {
1364                result.session_id = session_id.to_string();
1365            }
1366        }
1367        if let Some(thought) = obj.get("thought").and_then(|v| v.as_str()) {
1368            if !thought.is_empty() {
1369                result.events.push(JsonEvent {
1370                    event_type: "thinking".into(),
1371                    text: String::new(),
1372                    thinking: thought.to_string(),
1373                    tool_call: None,
1374                    tool_result: None,
1375                });
1376            }
1377        }
1378        if let Some(text) = obj.get("text").and_then(|v| v.as_str()) {
1379            result.final_text = text.to_string();
1380            if !text.is_empty() {
1381                result.events.push(JsonEvent {
1382                    event_type: "assistant".into(),
1383                    text: text.to_string(),
1384                    thinking: String::new(),
1385                    tool_call: None,
1386                    tool_result: None,
1387                });
1388            }
1389        }
1390        return true;
1391    }
1392
1393    if let Some(session_id) = obj.get("sessionId").and_then(|v| v.as_str()) {
1394        if !session_id.is_empty() {
1395            result.session_id = session_id.to_string();
1396        }
1397    }
1398
1399    match msg_type {
1400        "text" => {
1401            if let Some(text) = obj.get("data").and_then(|v| v.as_str()) {
1402                result.final_text.push_str(text);
1403                if !text.is_empty() {
1404                    result.events.push(JsonEvent {
1405                        event_type: "text_delta".into(),
1406                        text: text.to_string(),
1407                        thinking: String::new(),
1408                        tool_call: None,
1409                        tool_result: None,
1410                    });
1411                }
1412            }
1413            true
1414        }
1415        "thought" => {
1416            if let Some(thinking) = obj.get("data").and_then(|v| v.as_str()) {
1417                if !thinking.is_empty() {
1418                    result.events.push(JsonEvent {
1419                        event_type: "thinking_delta".into(),
1420                        text: String::new(),
1421                        thinking: thinking.to_string(),
1422                        tool_call: None,
1423                        tool_result: None,
1424                    });
1425                }
1426            }
1427            true
1428        }
1429        "end" => true,
1430        "error" => {
1431            let message = obj
1432                .get("message")
1433                .and_then(|v| v.as_str())
1434                .or_else(|| obj.get("data").and_then(|v| v.as_str()))
1435                .unwrap_or("error")
1436                .to_string();
1437            result.error = message.clone();
1438            result.events.push(JsonEvent {
1439                event_type: "error".into(),
1440                text: message,
1441                thinking: String::new(),
1442                tool_call: None,
1443                tool_result: None,
1444            });
1445            true
1446        }
1447        "max_turns_reached" | "auto_compact_start" | "auto_compact_end" => true,
1448        _ => false,
1449    }
1450}
1451
1452pub fn parse_grok_json(raw: &str) -> ParsedJsonOutput {
1453    let mut result = new_output("grok");
1454    let stripped = raw.trim();
1455    // Prefer one-shot object when the whole payload is a single JSON object
1456    // (including pretty-printed multi-line payloads).
1457    if stripped.starts_with('{') {
1458        if let Ok(obj) = serde_json::from_str::<serde_json::Value>(stripped) {
1459            if obj.get("type").and_then(|v| v.as_str()) == Some("error") {
1460                let message = obj
1461                    .get("message")
1462                    .and_then(|v| v.as_str())
1463                    .unwrap_or("error")
1464                    .to_string();
1465                result.error = message.clone();
1466                result.events.push(JsonEvent {
1467                    event_type: "error".into(),
1468                    text: message,
1469                    thinking: String::new(),
1470                    tool_call: None,
1471                    tool_result: None,
1472                });
1473                return result;
1474            }
1475            if obj.get("text").is_some()
1476                || obj.get("sessionId").is_some()
1477                || obj.get("stopReason").is_some()
1478            {
1479                apply_grok_obj(&mut result, &obj);
1480                return result;
1481            }
1482        }
1483    }
1484    for line in raw.lines() {
1485        if let Some(obj) = parse_json_line(line) {
1486            if !apply_grok_obj(&mut result, &obj) {
1487                result.unknown_json_lines.push(line.trim().to_string());
1488            }
1489        }
1490    }
1491    result
1492}
1493
1494pub fn parse_json_output(raw: &str, schema: &str) -> ParsedJsonOutput {
1495    match schema {
1496        "opencode" => parse_opencode_json(raw),
1497        "claude-code" => parse_claude_code_json(raw),
1498        "kimi" => parse_kimi_json(raw),
1499        "cursor-agent" => parse_cursor_agent_json(raw),
1500        "codex" => parse_codex_json(raw),
1501        "gemini" => parse_gemini_json(raw),
1502        "pi" => parse_pi_json(raw),
1503        "grok" => parse_grok_json(raw),
1504        _ => ParsedJsonOutput {
1505            schema_name: schema.into(),
1506            events: Vec::new(),
1507            final_text: String::new(),
1508            session_id: String::new(),
1509            error: format!("unknown schema: {schema}"),
1510            usage: BTreeMap::new(),
1511            cost_usd: 0.0,
1512            duration_ms: 0,
1513            unknown_json_lines: Vec::new(),
1514        },
1515    }
1516}
1517
1518fn truncate_to_char_limit(text: &str, max_chars: usize) -> Option<String> {
1519    text.char_indices()
1520        .nth(max_chars)
1521        .map(|(index, _)| text[..index].to_string())
1522}
1523
1524fn summarize_text(text: &str, max_lines: usize, max_chars: usize) -> String {
1525    let lines: Vec<&str> = text.trim().lines().collect();
1526    if lines.is_empty() {
1527        return String::new();
1528    }
1529    let mut clipped = lines
1530        .into_iter()
1531        .take(max_lines)
1532        .collect::<Vec<_>>()
1533        .join("\n");
1534    let mut truncated = text.trim().lines().count() > max_lines;
1535    if let Some(safe_clipped) = truncate_to_char_limit(&clipped, max_chars) {
1536        clipped = safe_clipped;
1537        clipped = clipped.trim_end().to_string();
1538        truncated = true;
1539    }
1540    if truncated {
1541        clipped.push_str(" …");
1542    }
1543    clipped
1544}
1545
1546fn parse_tool_arguments(arguments: &str) -> Option<serde_json::Map<String, serde_json::Value>> {
1547    let value: serde_json::Value = serde_json::from_str(arguments).ok()?;
1548    value.as_object().cloned()
1549}
1550
1551fn bash_command_preview(tool_call: &ToolCall) -> Option<String> {
1552    let args = parse_tool_arguments(&tool_call.arguments)?;
1553    for key in ["command", "cmd", "bash_command", "script"] {
1554        if let Some(value) = args.get(key).and_then(|value| value.as_str()) {
1555            let mut preview = value.trim().to_string();
1556            if preview.is_empty() {
1557                continue;
1558            }
1559            if let Some(safe_preview) = truncate_to_char_limit(&preview, 400) {
1560                preview = safe_preview.trim_end().to_string() + " …";
1561            }
1562            return Some(preview);
1563        }
1564    }
1565    None
1566}
1567
1568fn tool_preview(tool_name: &str, text: &str) -> String {
1569    match tool_name.to_ascii_lowercase().as_str() {
1570        "read" | "write" | "edit" | "multiedit" | "read_file" | "write_file" | "edit_file" => {
1571            String::new()
1572        }
1573        _ => summarize_text(text, 8, 400),
1574    }
1575}
1576
1577pub fn resolve_human_tty(tty: bool, force_color: Option<&str>, no_color: Option<&str>) -> bool {
1578    if force_color.is_some_and(|value| !value.is_empty()) {
1579        return true;
1580    }
1581    if no_color.is_some_and(|value| !value.is_empty()) {
1582        return false;
1583    }
1584    tty
1585}
1586
1587fn style(text: &str, code: &str, tty: bool) -> String {
1588    if tty {
1589        format!("\x1b[{code}m{text}\x1b[0m")
1590    } else {
1591        text.to_string()
1592    }
1593}
1594
1595pub struct FormattedRenderer {
1596    show_thinking: bool,
1597    tty: bool,
1598    seen_final_texts: BTreeSet<String>,
1599    tool_calls_by_id: BTreeMap<String, ToolCall>,
1600    pending_tool_call: Option<ToolCall>,
1601    streamed_assistant_buffer: String,
1602    plain_text_tool_work: bool,
1603}
1604
1605impl FormattedRenderer {
1606    pub fn new(show_thinking: bool, tty: bool) -> Self {
1607        Self {
1608            show_thinking,
1609            tty,
1610            seen_final_texts: BTreeSet::new(),
1611            tool_calls_by_id: BTreeMap::new(),
1612            pending_tool_call: None,
1613            streamed_assistant_buffer: String::new(),
1614            plain_text_tool_work: false,
1615        }
1616    }
1617
1618    pub fn render_output(&mut self, output: &ParsedJsonOutput) -> String {
1619        output
1620            .events
1621            .iter()
1622            .filter_map(|event| self.render_event(event))
1623            .collect::<Vec<_>>()
1624            .join("\n")
1625    }
1626
1627    pub fn render_event(&mut self, event: &JsonEvent) -> Option<String> {
1628        match event.event_type.as_str() {
1629            "text_delta" if !event.text.is_empty() => {
1630                self.streamed_assistant_buffer.push_str(&event.text);
1631                Some(self.render_message("assistant", &event.text))
1632            }
1633            "text" | "assistant" if !event.text.is_empty() => {
1634                if !self.streamed_assistant_buffer.is_empty()
1635                    && event.text == self.streamed_assistant_buffer
1636                {
1637                    self.seen_final_texts.insert(event.text.clone());
1638                    self.streamed_assistant_buffer.clear();
1639                    None
1640                } else {
1641                    self.streamed_assistant_buffer.clear();
1642                    Some(self.render_message("assistant", &event.text))
1643                }
1644            }
1645            "result" if !event.text.is_empty() => {
1646                if !self.streamed_assistant_buffer.is_empty()
1647                    && event.text == self.streamed_assistant_buffer
1648                {
1649                    self.seen_final_texts.insert(event.text.clone());
1650                    self.streamed_assistant_buffer.clear();
1651                    None
1652                } else if self.seen_final_texts.contains(&event.text) {
1653                    None
1654                } else {
1655                    self.streamed_assistant_buffer.clear();
1656                    Some(self.render_message("success", &event.text))
1657                }
1658            }
1659            "thinking" | "thinking_delta" if !event.thinking.is_empty() && self.show_thinking => {
1660                Some(self.render_message("thinking", &event.thinking))
1661            }
1662            "tool_use" | "tool_use_start" | "tool_call" => {
1663                if let Some(tool_call) = &event.tool_call {
1664                    self.streamed_assistant_buffer.clear();
1665                    if !tool_call.id.is_empty() {
1666                        self.tool_calls_by_id
1667                            .insert(tool_call.id.clone(), tool_call.clone());
1668                    }
1669                    self.pending_tool_call = Some(tool_call.clone());
1670                    self.plain_text_tool_work = true;
1671                    Some(self.render_tool_start(tool_call))
1672                } else {
1673                    None
1674                }
1675            }
1676            "tool_input_delta" if !event.text.is_empty() => {
1677                if let Some(tool_call) = &mut self.pending_tool_call {
1678                    tool_call.arguments.push_str(&event.text);
1679                    if !tool_call.id.is_empty() {
1680                        self.tool_calls_by_id
1681                            .insert(tool_call.id.clone(), tool_call.clone());
1682                    }
1683                }
1684                None
1685            }
1686            "tool_result" => event.tool_result.as_ref().map(|tool_result| {
1687                self.streamed_assistant_buffer.clear();
1688                self.render_tool_result(tool_result)
1689            }),
1690            "error" if !event.text.is_empty() => {
1691                self.streamed_assistant_buffer.clear();
1692                Some(self.render_message("error", &event.text))
1693            }
1694            _ => None,
1695        }
1696    }
1697
1698    fn render_message(&mut self, kind: &str, text: &str) -> String {
1699        if matches!(kind, "assistant" | "success") {
1700            self.seen_final_texts.insert(text.to_string());
1701        }
1702        let prefix = match kind {
1703            "assistant" => renderer_prefix(
1704                "💬",
1705                "[assistant]",
1706                "96",
1707                self.tty,
1708                self.plain_text_tool_work,
1709            ),
1710            "thinking" => renderer_prefix(
1711                "🧠",
1712                "[thinking]",
1713                "2;35",
1714                self.tty,
1715                self.plain_text_tool_work,
1716            ),
1717            "success" => renderer_prefix("✅", "[ok]", "92", self.tty, self.plain_text_tool_work),
1718            _ => renderer_prefix("❌", "[error]", "91", self.tty, self.plain_text_tool_work),
1719        };
1720        with_prefix(&prefix, text)
1721    }
1722
1723    fn render_tool_start(&self, tool_call: &ToolCall) -> String {
1724        let prefix = prefix("🛠️", "[tool:start]", "94", self.tty);
1725        let mut detail = tool_call.name.clone();
1726        if let Some(preview) = bash_command_preview(tool_call) {
1727            detail.push_str(": ");
1728            detail.push_str(&preview);
1729        }
1730        with_prefix(&prefix, &detail)
1731    }
1732
1733    fn render_tool_result(&self, tool_result: &ToolResult) -> String {
1734        let prefix = prefix("📎", "[tool:result]", "36", self.tty);
1735        let tool_call = self
1736            .tool_calls_by_id
1737            .get(&tool_result.tool_call_id)
1738            .or(self.pending_tool_call.as_ref());
1739        let tool_name = tool_call
1740            .map(|tool_call| tool_call.name.clone())
1741            .unwrap_or_else(|| "tool".into());
1742        let mut summary = format!(
1743            "{} ({})",
1744            tool_name,
1745            if tool_result.is_error { "error" } else { "ok" }
1746        );
1747        if let Some(tool_call) = tool_call {
1748            if let Some(preview) = bash_command_preview(tool_call) {
1749                summary.push_str(": ");
1750                summary.push_str(&preview);
1751            }
1752        }
1753        let preview = tool_preview(&tool_name, &tool_result.content);
1754        if !preview.is_empty() {
1755            summary.push('\n');
1756            summary.push_str(&preview);
1757        }
1758        with_prefix(&prefix, &summary)
1759    }
1760}
1761
1762fn prefix(emoji: &str, plain: &str, color_code: &str, tty: bool) -> String {
1763    if tty {
1764        style(emoji, color_code, true)
1765    } else {
1766        plain.to_string()
1767    }
1768}
1769
1770fn renderer_prefix(
1771    emoji: &str,
1772    plain: &str,
1773    color_code: &str,
1774    tty: bool,
1775    plain_text_tool_work: bool,
1776) -> String {
1777    if tty {
1778        return style(emoji, color_code, true);
1779    }
1780    if plain_text_tool_work && matches!(plain, "[assistant]" | "[thinking]" | "[ok]" | "[error]") {
1781        return plain.to_string();
1782    }
1783    plain.to_string()
1784}
1785
1786fn with_prefix(prefix: &str, text: &str) -> String {
1787    text.lines()
1788        .map(|line| {
1789            if line.is_empty() {
1790                prefix.to_string()
1791            } else {
1792                format!("{prefix} {line}")
1793            }
1794        })
1795        .collect::<Vec<_>>()
1796        .join("\n")
1797}
1798
1799pub struct StructuredStreamProcessor {
1800    schema: String,
1801    renderer: FormattedRenderer,
1802    output: ParsedJsonOutput,
1803    buffer: String,
1804    unknown_json_lines: Vec<String>,
1805}
1806
1807impl StructuredStreamProcessor {
1808    pub fn new(schema: &str, renderer: FormattedRenderer) -> Self {
1809        Self {
1810            schema: schema.into(),
1811            renderer,
1812            output: new_output(schema),
1813            buffer: String::new(),
1814            unknown_json_lines: Vec::new(),
1815        }
1816    }
1817
1818    pub fn output(&self) -> &ParsedJsonOutput {
1819        &self.output
1820    }
1821
1822    pub fn feed(&mut self, chunk: &str) -> String {
1823        self.buffer.push_str(chunk);
1824        let mut rendered = Vec::new();
1825        while let Some(index) = self.buffer.find('\n') {
1826            let line = self.buffer[..index].to_string();
1827            self.buffer = self.buffer[index + 1..].to_string();
1828            if let Some(obj) = parse_json_line(&line) {
1829                let before = parser_state(&self.output);
1830                let event_count = self.output.events.len();
1831                let recognized = self.apply(&obj);
1832                let after = parser_state(&self.output);
1833                if before == after && !recognized {
1834                    self.unknown_json_lines.push(line.trim().to_string());
1835                }
1836                for event in &self.output.events[event_count..] {
1837                    if let Some(text) = self.renderer.render_event(event) {
1838                        rendered.push(text);
1839                    }
1840                }
1841            }
1842        }
1843        rendered.join("\n")
1844    }
1845
1846    pub fn finish(&mut self) -> String {
1847        if self.buffer.trim().is_empty() {
1848            return String::new();
1849        }
1850        let line = std::mem::take(&mut self.buffer);
1851        if let Some(obj) = parse_json_line(&line) {
1852            let before = parser_state(&self.output);
1853            let event_count = self.output.events.len();
1854            let recognized = self.apply(&obj);
1855            let after = parser_state(&self.output);
1856            if before == after && !recognized {
1857                self.unknown_json_lines.push(line.trim().to_string());
1858            }
1859            return self.output.events[event_count..]
1860                .iter()
1861                .filter_map(|event| self.renderer.render_event(event))
1862                .collect::<Vec<_>>()
1863                .join("\n");
1864        }
1865        String::new()
1866    }
1867
1868    pub fn take_unknown_json_lines(&mut self) -> Vec<String> {
1869        std::mem::take(&mut self.unknown_json_lines)
1870    }
1871
1872    fn apply(&mut self, obj: &serde_json::Value) -> bool {
1873        match self.schema.as_str() {
1874            "opencode" => {
1875                apply_opencode_obj(&mut self.output, obj);
1876                false
1877            }
1878            "claude-code" => apply_claude_obj(&mut self.output, obj),
1879            "kimi" => {
1880                apply_kimi_obj(&mut self.output, obj);
1881                false
1882            }
1883            "cursor-agent" => {
1884                apply_cursor_agent_obj(&mut self.output, obj);
1885                false
1886            }
1887            "codex" => apply_codex_obj(&mut self.output, obj),
1888            "gemini" => apply_gemini_obj(&mut self.output, obj),
1889            "pi" => apply_pi_obj(&mut self.output, obj),
1890            "grok" => apply_grok_obj(&mut self.output, obj),
1891            _ => false,
1892        }
1893    }
1894}
1895
1896pub fn render_parsed(output: &ParsedJsonOutput, show_thinking: bool, tty: bool) -> String {
1897    let mut renderer = FormattedRenderer::new(show_thinking, tty);
1898    let rendered = renderer.render_output(output);
1899    if rendered.is_empty() {
1900        output.final_text.clone()
1901    } else {
1902        rendered
1903    }
1904}