1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use uuid::Uuid;
4
5use super::content_blocks::{ContentBlock, ImageBlock, ImageSource, TextBlock};
6use super::control::{ControlRequest, ControlResponse};
7use super::message_types::{MessageContent, UserMessage};
8
9#[allow(clippy::large_enum_variant)]
13#[derive(Debug, Clone, Serialize, Deserialize)]
14#[serde(tag = "type", rename_all = "snake_case")]
15pub enum ClaudeInput {
16 User(UserMessage),
18
19 ControlRequest(ControlRequest),
21
22 ControlResponse(ControlResponse),
24
25 #[serde(untagged)]
27 Raw(Value),
28}
29
30impl ClaudeInput {
31 pub fn user_message(text: impl Into<String>, session_id: Uuid) -> Self {
33 Self::text_user_message(text, Some(session_id))
34 }
35
36 pub fn user_message_without_session(text: impl Into<String>) -> Self {
39 Self::text_user_message(text, None)
40 }
41
42 fn text_user_message(text: impl Into<String>, session_id: Option<Uuid>) -> Self {
43 ClaudeInput::User(UserMessage {
44 message: MessageContent {
45 role: super::MessageRole::User,
46 content: vec![ContentBlock::Text(TextBlock {
47 text: text.into(),
48 citations: Vec::new(),
49 })],
50 },
51 session_id,
52 parent_tool_use_id: None,
53 uuid: None,
54 timestamp: None,
55 tool_use_result: None,
56 subagent_type: None,
57 task_description: None,
58 origin: None,
59 priority: None,
60 is_synthetic: None,
61 should_query: None,
62 is_meta: None,
63 is_visible_in_transcript_only: None,
64 is_virtual: None,
65 is_compact_summary: None,
66 summarize_metadata: None,
67 mcp_meta: None,
68 tool_result_meta: None,
69 source_tool_use_id: None,
70 source_tool_assistant_uuid: None,
71 image_paste_ids: None,
72 client_platform: None,
73 inbound_origin: None,
74 is_replay: None,
75 file_attachments: None,
76 })
77 }
78
79 pub fn user_message_blocks(blocks: Vec<ContentBlock>, session_id: Uuid) -> Self {
81 ClaudeInput::User(UserMessage {
82 message: MessageContent {
83 role: super::MessageRole::User,
84 content: blocks,
85 },
86 session_id: Some(session_id),
87 parent_tool_use_id: None,
88 uuid: None,
89 timestamp: None,
90 tool_use_result: None,
91 subagent_type: None,
92 task_description: None,
93 origin: None,
94 priority: None,
95 is_synthetic: None,
96 should_query: None,
97 is_meta: None,
98 is_visible_in_transcript_only: None,
99 is_virtual: None,
100 is_compact_summary: None,
101 summarize_metadata: None,
102 mcp_meta: None,
103 tool_result_meta: None,
104 source_tool_use_id: None,
105 source_tool_assistant_uuid: None,
106 image_paste_ids: None,
107 client_platform: None,
108 inbound_origin: None,
109 is_replay: None,
110 file_attachments: None,
111 })
112 }
113
114 pub fn interrupt(request_id: impl Into<String>) -> Self {
125 ClaudeInput::ControlRequest(ControlRequest {
126 request_id: request_id.into(),
127 request: super::ControlRequestPayload::Interrupt,
128 })
129 }
130
131 pub fn user_message_with_image(
134 image_data: String,
135 media_type: super::MediaType,
136 text: Option<String>,
137 session_id: Uuid,
138 ) -> Result<Self, String> {
139 match &media_type {
141 super::MediaType::Jpeg
142 | super::MediaType::Png
143 | super::MediaType::Gif
144 | super::MediaType::Webp => {}
145 other => {
146 return Err(format!(
147 "Invalid media type '{}'. Only JPEG, PNG, GIF, and WebP are supported.",
148 other
149 ));
150 }
151 }
152
153 let mut blocks = vec![ContentBlock::Image(ImageBlock {
154 source: ImageSource {
155 source_type: super::ImageSourceType::Base64,
156 media_type,
157 data: image_data,
158 },
159 })];
160
161 if let Some(text_content) = text {
162 blocks.push(ContentBlock::Text(TextBlock {
163 text: text_content,
164 citations: Vec::new(),
165 }));
166 }
167
168 Ok(Self::user_message_blocks(blocks, session_id))
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn test_interrupt_serializes_to_control_request_envelope() {
178 let input = ClaudeInput::interrupt("interrupt-abc");
179 assert_eq!(
180 serde_json::to_value(&input).unwrap(),
181 serde_json::json!({
182 "type": "control_request",
183 "request_id": "interrupt-abc",
184 "request": {"subtype": "interrupt"}
185 })
186 );
187 }
188
189 #[test]
190 fn test_serialize_user_message() {
191 let session_uuid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
192 let input = ClaudeInput::user_message("Hello, Claude!", session_uuid);
193 let json = serde_json::to_string(&input).unwrap();
194 assert!(json.contains("\"type\":\"user\""));
195 assert!(json.contains("\"role\":\"user\""));
196 assert!(json.contains("\"text\":\"Hello, Claude!\""));
197 assert!(json.contains("550e8400-e29b-41d4-a716-446655440000"));
198 }
199
200 #[test]
201 fn test_serialize_user_message_without_session() {
202 let input = ClaudeInput::user_message_without_session("Hello, Claude!");
203 let json = serde_json::to_string(&input).unwrap();
204 assert!(json.contains("\"type\":\"user\""));
205 assert!(!json.contains("session_id"));
206 }
207}