use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UsageInfo { pub input_tokens: u64, pub output_tokens: u64, #[serde(default)] pub cache_creation_input_tokens: Option, #[serde(default)] pub cache_read_input_tokens: Option, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] #[serde(rename_all = "snake_case")] pub enum CharacterState { #[default] Idle, Thinking, Typing, Searching, Coding, Mcp, Permission, Success, Error, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(rename_all = "snake_case")] pub enum ConnectionStatus { #[default] Disconnected, Connecting, Connected, Error, } #[allow(dead_code)] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TerminalLine { pub id: String, #[serde(rename = "type")] pub line_type: String, pub content: String, pub timestamp: String, #[serde(skip_serializing_if = "Option::is_none")] pub tool_name: Option, } #[allow(dead_code)] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PermissionRequest { pub id: String, pub tool: String, pub description: String, pub input: serde_json::Value, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PermissionDenial { pub tool_name: String, pub tool_use_id: String, pub tool_input: serde_json::Value, } /// Rate limit information from a `rate_limit_event` message. /// All fields are optional to ensure forward-compatibility as the Claude CLI evolves. #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct RateLimitInfo { #[serde(default)] pub requests_limit: Option, #[serde(default)] pub requests_remaining: Option, #[serde(default)] pub requests_reset: Option, #[serde(default)] pub tokens_limit: Option, #[serde(default)] pub tokens_remaining: Option, #[serde(default)] pub tokens_reset: Option, #[serde(default)] pub retry_after_ms: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type")] pub enum ClaudeMessage { #[serde(rename = "system")] System { subtype: String, #[serde(default)] session_id: Option, #[serde(default)] cwd: Option, #[serde(default)] tools: Option>, }, #[serde(rename = "assistant")] Assistant { message: AssistantMessageContent, #[serde(default)] parent_tool_use_id: Option, }, #[serde(rename = "user")] User { message: UserMessageContent }, #[serde(rename = "stream_event")] StreamEvent { event: StreamEventData }, #[serde(rename = "result")] Result { subtype: String, #[serde(default)] result: Option, #[serde(default)] duration_ms: Option, #[serde(default)] num_turns: Option, #[serde(default)] permission_denials: Option>, #[serde(default)] usage: Option, }, #[serde(rename = "rate_limit_event")] RateLimitEvent { #[serde(default)] rate_limit_info: RateLimitInfo, }, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AssistantMessageContent { pub content: Vec, #[serde(default)] pub model: Option, #[serde(default)] pub stop_reason: Option, #[serde(default)] pub usage: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UserMessageContent { pub content: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type")] pub enum ContentBlock { #[serde(rename = "text")] Text { text: String }, #[serde(rename = "thinking")] Thinking { thinking: String }, #[serde(rename = "tool_use")] ToolUse { id: String, name: String, input: serde_json::Value, }, #[serde(rename = "tool_result")] ToolResult { tool_use_id: String, content: serde_json::Value, #[serde(default)] is_error: Option, }, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StreamEventData { #[serde(rename = "type")] pub event_type: String, #[serde(default)] pub index: Option, #[serde(default)] pub content_block: Option, #[serde(default)] pub delta: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ContentBlockStart { #[serde(rename = "type")] pub block_type: String, #[serde(default)] pub id: Option, #[serde(default)] pub name: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DeltaContent { #[serde(rename = "type")] pub delta_type: String, #[serde(default)] pub text: Option, #[serde(default)] pub thinking: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StateChangeEvent { pub state: CharacterState, pub tool_name: Option, #[serde(skip_serializing_if = "Option::is_none")] pub conversation_id: Option, } /// Cost information for a message #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MessageCost { pub input_tokens: u64, pub output_tokens: u64, pub cost_usd: f64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OutputEvent { pub line_type: String, pub content: String, pub tool_name: Option, #[serde(skip_serializing_if = "Option::is_none")] pub conversation_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub cost: Option, #[serde(skip_serializing_if = "Option::is_none")] pub parent_tool_use_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PermissionPromptEventItem { pub id: String, pub tool_name: String, pub tool_input: serde_json::Value, pub description: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PermissionPromptEvent { pub permissions: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub conversation_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConnectionEvent { pub status: ConnectionStatus, #[serde(skip_serializing_if = "Option::is_none")] pub conversation_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SessionEvent { pub session_id: String, #[serde(skip_serializing_if = "Option::is_none")] pub conversation_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WorkingDirectoryEvent { pub directory: String, #[serde(skip_serializing_if = "Option::is_none")] pub conversation_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct QuestionOption { pub label: String, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UserQuestionEvent { pub id: String, pub question: String, pub header: Option, pub options: Vec, pub multi_select: bool, #[serde(skip_serializing_if = "Option::is_none")] pub conversation_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AgentStartEvent { pub tool_use_id: String, #[serde(skip_serializing_if = "Option::is_none")] pub agent_id: Option, pub description: String, pub subagent_type: String, pub started_at: u64, #[serde(skip_serializing_if = "Option::is_none")] pub conversation_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub parent_tool_use_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AgentEndEvent { pub tool_use_id: String, pub ended_at: u64, pub is_error: bool, #[serde(skip_serializing_if = "Option::is_none")] pub conversation_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub duration_ms: Option, #[serde(skip_serializing_if = "Option::is_none")] pub num_turns: Option, #[serde(skip_serializing_if = "Option::is_none")] pub last_assistant_message: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TodoItem { pub content: String, pub status: String, // "pending", "in_progress", or "completed" #[serde(rename = "activeForm")] pub active_form: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TodoUpdateEvent { pub todos: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub conversation_id: Option, } #[cfg(test)] mod tests { use super::*; #[test] fn test_character_state_default() { let state = CharacterState::default(); assert_eq!(state, CharacterState::Idle); } #[test] fn test_connection_status_default() { let status = ConnectionStatus::default(); matches!(status, ConnectionStatus::Disconnected); } #[test] fn test_character_state_serialization() { let state = CharacterState::Thinking; let serialized = serde_json::to_string(&state).unwrap(); assert_eq!(serialized, "\"thinking\""); let deserialized: CharacterState = serde_json::from_str(&serialized).unwrap(); assert_eq!(deserialized, CharacterState::Thinking); } #[test] fn test_all_character_states_serialize() { let states = vec![ (CharacterState::Idle, "\"idle\""), (CharacterState::Thinking, "\"thinking\""), (CharacterState::Typing, "\"typing\""), (CharacterState::Searching, "\"searching\""), (CharacterState::Coding, "\"coding\""), (CharacterState::Mcp, "\"mcp\""), (CharacterState::Permission, "\"permission\""), (CharacterState::Success, "\"success\""), (CharacterState::Error, "\"error\""), ]; for (state, expected) in states { let serialized = serde_json::to_string(&state).unwrap(); assert_eq!(serialized, expected, "Failed for state: {:?}", state); } } #[test] fn test_terminal_line_serialization() { let line = TerminalLine { id: "test-123".to_string(), line_type: "assistant".to_string(), content: "Hello, world!".to_string(), timestamp: "2024-01-01T00:00:00Z".to_string(), tool_name: None, }; let serialized = serde_json::to_string(&line).unwrap(); assert!(serialized.contains("\"type\":\"assistant\"")); assert!(serialized.contains("\"content\":\"Hello, world!\"")); assert!(!serialized.contains("tool_name")); } #[test] fn test_terminal_line_with_tool_name() { let line = TerminalLine { id: "test-456".to_string(), line_type: "tool".to_string(), content: "Reading file...".to_string(), timestamp: "2024-01-01T00:00:00Z".to_string(), tool_name: Some("Read".to_string()), }; let serialized = serde_json::to_string(&line).unwrap(); assert!(serialized.contains("\"tool_name\":\"Read\"")); } #[test] fn test_content_block_text() { let block = ContentBlock::Text { text: "Hello!".to_string(), }; let serialized = serde_json::to_string(&block).unwrap(); assert!(serialized.contains("\"type\":\"text\"")); assert!(serialized.contains("\"text\":\"Hello!\"")); } #[test] fn test_content_block_tool_use() { let block = ContentBlock::ToolUse { id: "tool-123".to_string(), name: "Read".to_string(), input: serde_json::json!({"file_path": "/test.txt"}), }; let serialized = serde_json::to_string(&block).unwrap(); assert!(serialized.contains("\"type\":\"tool_use\"")); assert!(serialized.contains("\"name\":\"Read\"")); } #[test] fn test_state_change_event() { let event = StateChangeEvent { state: CharacterState::Coding, tool_name: Some("Edit".to_string()), conversation_id: None, }; let serialized = serde_json::to_string(&event).unwrap(); assert!(serialized.contains("\"state\":\"coding\"")); assert!(serialized.contains("\"tool_name\":\"Edit\"")); } #[test] fn test_output_event() { let event = OutputEvent { line_type: "assistant".to_string(), content: "Test output".to_string(), tool_name: None, conversation_id: None, cost: None, parent_tool_use_id: None, }; let serialized = serde_json::to_string(&event).unwrap(); assert!(serialized.contains("\"line_type\":\"assistant\"")); assert!(serialized.contains("\"content\":\"Test output\"")); } #[test] fn test_output_event_with_cost() { let event = OutputEvent { line_type: "assistant".to_string(), content: "Test output".to_string(), tool_name: None, conversation_id: Some("conv-123".to_string()), cost: Some(MessageCost { input_tokens: 100, output_tokens: 50, cost_usd: 0.005, }), parent_tool_use_id: None, }; let serialized = serde_json::to_string(&event).unwrap(); assert!(serialized.contains("\"cost\":")); assert!(serialized.contains("\"input_tokens\":100")); assert!(serialized.contains("\"output_tokens\":50")); } #[test] fn test_rate_limit_info_default() { let info = RateLimitInfo::default(); assert!(info.requests_limit.is_none()); assert!(info.requests_remaining.is_none()); assert!(info.requests_reset.is_none()); assert!(info.tokens_limit.is_none()); assert!(info.tokens_remaining.is_none()); assert!(info.tokens_reset.is_none()); assert!(info.retry_after_ms.is_none()); } #[test] fn test_rate_limit_event_deserialization_empty_info() { let json = r#"{"type":"rate_limit_event","rate_limit_info":{}}"#; let msg: ClaudeMessage = serde_json::from_str(json).unwrap(); assert!(matches!(msg, ClaudeMessage::RateLimitEvent { .. })); } #[test] fn test_rate_limit_event_deserialization_no_info() { // rate_limit_info field is optional via #[serde(default)] let json = r#"{"type":"rate_limit_event"}"#; let msg: ClaudeMessage = serde_json::from_str(json).unwrap(); assert!(matches!(msg, ClaudeMessage::RateLimitEvent { .. })); } #[test] fn test_rate_limit_event_deserialization_with_data() { let json = r#"{ "type": "rate_limit_event", "rate_limit_info": { "requests_limit": 1000, "requests_remaining": 0, "requests_reset": "2024-01-01T00:01:00Z", "tokens_limit": 50000, "tokens_remaining": 0, "tokens_reset": "2024-01-01T00:01:00Z", "retry_after_ms": 60000 } }"#; let msg: ClaudeMessage = serde_json::from_str(json).unwrap(); if let ClaudeMessage::RateLimitEvent { rate_limit_info } = msg { assert_eq!(rate_limit_info.requests_limit, Some(1000)); assert_eq!(rate_limit_info.requests_remaining, Some(0)); assert_eq!( rate_limit_info.requests_reset, Some("2024-01-01T00:01:00Z".to_string()) ); assert_eq!(rate_limit_info.retry_after_ms, Some(60000)); } else { panic!("Expected RateLimitEvent variant"); } } #[test] fn test_rate_limit_event_ignores_unknown_fields() { // Ensures forward-compat: unknown fields in rate_limit_info are silently ignored let json = r#"{ "type": "rate_limit_event", "rate_limit_info": { "requests_remaining": 0, "some_future_field": "some_value" } }"#; let msg: ClaudeMessage = serde_json::from_str(json).unwrap(); if let ClaudeMessage::RateLimitEvent { rate_limit_info } = msg { assert_eq!(rate_limit_info.requests_remaining, Some(0)); } else { panic!("Expected RateLimitEvent variant"); } } }