//! Claude Code event handling — dispatches JSON events from the CLI to message channels. mod stream; #[cfg(test)] mod tests; use super::parse::{parse_assistant_message, parse_tool_results}; use crate::agents::TokenUsage; use crate::llm::types::Message; use crate::slog; use std::sync::atomic::{AtomicBool, Ordering}; use stream::handle_stream_event; /// Dispatch a top-level JSON event from the Claude Code CLI. /// /// Routes the event to the appropriate handler based on `type`, emitting tokens, /// thinking output, activity signals, and parsed messages to their respective channels. /// Returns `true` only for `"result"` events, which signal that the CLI turn is complete. #[allow(clippy::too_many_arguments)] pub(super) fn process_json_event( json: &serde_json::Value, token_tx: &tokio::sync::mpsc::UnboundedSender, thinking_tx: &tokio::sync::mpsc::UnboundedSender, activity_tx: &tokio::sync::mpsc::UnboundedSender, msg_tx: &std::sync::mpsc::Sender, sid_tx: &mut Option>, usage_tx: &mut Option>, auth_failed: &AtomicBool, ) -> bool { let event_type = match json.get("type").and_then(|t| t.as_str()) { Some(t) => t, None => return false, }; // Capture session_id from the first event that carries it if let Some(tx) = sid_tx.take() { if let Some(sid) = json.get("session_id").and_then(|s| s.as_str()) { slog!("[pty-debug] CAPTURED session_id: {}", sid); let _ = tx.send(sid.to_string()); } else { *sid_tx = Some(tx); } } // Detect authentication_failed at the top level of any event. if json.get("error").and_then(|e| e.as_str()) == Some("authentication_failed") { slog!("[pty-debug] Detected authentication_failed error"); auth_failed.store(true, Ordering::Relaxed); } match event_type { "stream_event" => { if let Some(event) = json.get("event") { handle_stream_event(event, token_tx, thinking_tx, activity_tx); } false } "assistant" => { if let Some(message) = json.get("message") && let Some(content) = message.get("content").and_then(|c| c.as_array()) { // Fire activity signals for tool_use blocks as a fallback path. // The primary path is via stream_event → content_block_start (real-time), // but assistant events also carry tool_use blocks and serve as a reliable // backup if stream_event delivery is delayed or missed. for block in content { if block.get("type").and_then(|t| t.as_str()) == Some("tool_use") && let Some(name) = block.get("name").and_then(|n| n.as_str()) { let _ = activity_tx.send(name.to_string()); } } parse_assistant_message(content, msg_tx); } false } "user" => { if let Some(message) = json.get("message") && let Some(content) = message.get("content").and_then(|c| c.as_array()) { parse_tool_results(content, msg_tx); } false } "result" => { if let Some(tx) = usage_tx.take() && let Some(usage) = TokenUsage::from_result_event(json) { let _ = tx.send(usage); } true } // system, rate_limit_event, and unknown types are no-ops _ => false, } }