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 seeded_summon: None,
77 client_composed: None,
78 })
79 }
80
81 pub fn user_message_blocks(blocks: Vec<ContentBlock>, session_id: Uuid) -> Self {
83 ClaudeInput::User(UserMessage {
84 message: MessageContent {
85 role: super::MessageRole::User,
86 content: blocks,
87 },
88 session_id: Some(session_id),
89 parent_tool_use_id: None,
90 uuid: None,
91 timestamp: None,
92 tool_use_result: None,
93 subagent_type: None,
94 task_description: None,
95 origin: None,
96 priority: None,
97 is_synthetic: None,
98 should_query: None,
99 is_meta: None,
100 is_visible_in_transcript_only: None,
101 is_virtual: None,
102 is_compact_summary: None,
103 summarize_metadata: None,
104 mcp_meta: None,
105 tool_result_meta: None,
106 source_tool_use_id: None,
107 source_tool_assistant_uuid: None,
108 image_paste_ids: None,
109 client_platform: None,
110 inbound_origin: None,
111 is_replay: None,
112 file_attachments: None,
113 seeded_summon: None,
114 client_composed: None,
115 })
116 }
117
118 pub fn interrupt(request_id: impl Into<String>) -> Self {
129 ClaudeInput::ControlRequest(ControlRequest {
130 request_id: request_id.into(),
131 request: super::ControlRequestPayload::Interrupt,
132 })
133 }
134
135 pub fn user_message_with_image(
138 image_data: String,
139 media_type: super::MediaType,
140 text: Option<String>,
141 session_id: Uuid,
142 ) -> Result<Self, String> {
143 match &media_type {
145 super::MediaType::Jpeg
146 | super::MediaType::Png
147 | super::MediaType::Gif
148 | super::MediaType::Webp => {}
149 other => {
150 return Err(format!(
151 "Invalid media type '{}'. Only JPEG, PNG, GIF, and WebP are supported.",
152 other
153 ));
154 }
155 }
156
157 let mut blocks = vec![ContentBlock::Image(ImageBlock {
158 source: ImageSource {
159 source_type: super::ImageSourceType::Base64,
160 media_type,
161 data: image_data,
162 },
163 })];
164
165 if let Some(text_content) = text {
166 blocks.push(ContentBlock::Text(TextBlock {
167 text: text_content,
168 citations: Vec::new(),
169 }));
170 }
171
172 Ok(Self::user_message_blocks(blocks, session_id))
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn test_interrupt_serializes_to_control_request_envelope() {
182 let input = ClaudeInput::interrupt("interrupt-abc");
183 assert_eq!(
184 serde_json::to_value(&input).unwrap(),
185 serde_json::json!({
186 "type": "control_request",
187 "request_id": "interrupt-abc",
188 "request": {"subtype": "interrupt"}
189 })
190 );
191 }
192
193 #[test]
194 fn test_serialize_user_message() {
195 let session_uuid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
196 let input = ClaudeInput::user_message("Hello, Claude!", session_uuid);
197 let json = serde_json::to_string(&input).unwrap();
198 assert!(json.contains("\"type\":\"user\""));
199 assert!(json.contains("\"role\":\"user\""));
200 assert!(json.contains("\"text\":\"Hello, Claude!\""));
201 assert!(json.contains("550e8400-e29b-41d4-a716-446655440000"));
202 }
203
204 #[test]
205 fn test_serialize_user_message_without_session() {
206 let input = ClaudeInput::user_message_without_session("Hello, Claude!");
207 let json = serde_json::to_string(&input).unwrap();
208 assert!(json.contains("\"type\":\"user\""));
209 assert!(!json.contains("session_id"));
210 }
211}