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 agent_id: None,
57 subagent_type: None,
58 task_description: None,
59 origin: None,
60 priority: None,
61 is_synthetic: None,
62 should_query: None,
63 is_meta: None,
64 is_visible_in_transcript_only: None,
65 is_virtual: None,
66 is_compact_summary: None,
67 summarize_metadata: None,
68 mcp_meta: None,
69 tool_result_meta: None,
70 source_tool_use_id: None,
71 source_tool_assistant_uuid: None,
72 image_paste_ids: None,
73 client_platform: None,
74 inbound_origin: None,
75 is_replay: None,
76 file_attachments: None,
77 seeded_summon: None,
78 client_composed: None,
79 text_not_typed: None,
80 initiator: None,
81 pasted_content: None,
82 historical: None,
83 server_received_wall_ms: None,
84 refused_message_id: None,
85 requeue_if_interrupted_within_ms: None,
86 ultrathink_typed: None,
87 tool_states: None,
88 plugin_prompt: None,
89 })
90 }
91
92 pub fn user_message_blocks(blocks: Vec<ContentBlock>, session_id: Uuid) -> Self {
94 ClaudeInput::User(UserMessage {
95 message: MessageContent {
96 role: super::MessageRole::User,
97 content: blocks,
98 },
99 session_id: Some(session_id),
100 parent_tool_use_id: None,
101 uuid: None,
102 timestamp: None,
103 tool_use_result: None,
104 agent_id: None,
105 subagent_type: None,
106 task_description: None,
107 origin: None,
108 priority: None,
109 is_synthetic: None,
110 should_query: None,
111 is_meta: None,
112 is_visible_in_transcript_only: None,
113 is_virtual: None,
114 is_compact_summary: None,
115 summarize_metadata: None,
116 mcp_meta: None,
117 tool_result_meta: None,
118 source_tool_use_id: None,
119 source_tool_assistant_uuid: None,
120 image_paste_ids: None,
121 client_platform: None,
122 inbound_origin: None,
123 is_replay: None,
124 file_attachments: None,
125 seeded_summon: None,
126 client_composed: None,
127 text_not_typed: None,
128 initiator: None,
129 pasted_content: None,
130 historical: None,
131 server_received_wall_ms: None,
132 refused_message_id: None,
133 requeue_if_interrupted_within_ms: None,
134 ultrathink_typed: None,
135 tool_states: None,
136 plugin_prompt: None,
137 })
138 }
139
140 pub fn interrupt(request_id: impl Into<String>) -> Self {
151 ClaudeInput::ControlRequest(ControlRequest {
152 request_id: request_id.into(),
153 request: super::ControlRequestPayload::Interrupt,
154 })
155 }
156
157 pub fn apply_flag_settings(
165 request_id: impl Into<String>,
166 settings: super::ApplyFlagSettingsRequest,
167 ) -> Self {
168 ClaudeInput::ControlRequest(ControlRequest {
169 request_id: request_id.into(),
170 request: super::ControlRequestPayload::ApplyFlagSettings(settings),
171 })
172 }
173
174 pub fn user_message_with_image(
177 image_data: String,
178 media_type: super::MediaType,
179 text: Option<String>,
180 session_id: Uuid,
181 ) -> Result<Self, String> {
182 match &media_type {
184 super::MediaType::Jpeg
185 | super::MediaType::Png
186 | super::MediaType::Gif
187 | super::MediaType::Webp => {}
188 other => {
189 return Err(format!(
190 "Invalid media type '{}'. Only JPEG, PNG, GIF, and WebP are supported.",
191 other
192 ));
193 }
194 }
195
196 let mut blocks = vec![ContentBlock::Image(ImageBlock {
197 source: ImageSource {
198 source_type: super::ImageSourceType::Base64,
199 media_type,
200 data: image_data,
201 },
202 })];
203
204 if let Some(text_content) = text {
205 blocks.push(ContentBlock::Text(TextBlock {
206 text: text_content,
207 citations: Vec::new(),
208 }));
209 }
210
211 Ok(Self::user_message_blocks(blocks, session_id))
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn test_interrupt_serializes_to_control_request_envelope() {
221 let input = ClaudeInput::interrupt("interrupt-abc");
222 assert_eq!(
223 serde_json::to_value(&input).unwrap(),
224 serde_json::json!({
225 "type": "control_request",
226 "request_id": "interrupt-abc",
227 "request": {"subtype": "interrupt"}
228 })
229 );
230 }
231
232 #[test]
233 fn test_serialize_user_message() {
234 let session_uuid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
235 let input = ClaudeInput::user_message("Hello, Claude!", session_uuid);
236 let json = serde_json::to_string(&input).unwrap();
237 assert!(json.contains("\"type\":\"user\""));
238 assert!(json.contains("\"role\":\"user\""));
239 assert!(json.contains("\"text\":\"Hello, Claude!\""));
240 assert!(json.contains("550e8400-e29b-41d4-a716-446655440000"));
241 }
242
243 #[test]
244 fn test_serialize_user_message_without_session() {
245 let input = ClaudeInput::user_message_without_session("Hello, Claude!");
246 let json = serde_json::to_string(&input).unwrap();
247 assert!(json.contains("\"type\":\"user\""));
248 assert!(!json.contains("session_id"));
249 }
250}