Skip to main content

claude_codes/io/
claude_input.rs

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/// Top-level enum for all possible Claude input messages.
10///
11/// Keep variants unboxed so pattern matches and constructors stay ergonomic.
12#[allow(clippy::large_enum_variant)]
13#[derive(Debug, Clone, Serialize, Deserialize)]
14#[serde(tag = "type", rename_all = "snake_case")]
15pub enum ClaudeInput {
16    /// User message input
17    User(UserMessage),
18
19    /// Control request (for initialization handshake)
20    ControlRequest(ControlRequest),
21
22    /// Control response (for tool permission responses)
23    ControlResponse(ControlResponse),
24
25    /// Raw JSON for untyped messages
26    #[serde(untagged)]
27    Raw(Value),
28}
29
30impl ClaudeInput {
31    /// Create a simple text user message
32    pub fn user_message(text: impl Into<String>, session_id: Uuid) -> Self {
33        ClaudeInput::User(UserMessage {
34            message: MessageContent {
35                role: super::MessageRole::User,
36                content: vec![ContentBlock::Text(TextBlock {
37                    text: text.into(),
38                    citations: Vec::new(),
39                })],
40            },
41            session_id: Some(session_id),
42            parent_tool_use_id: None,
43            uuid: None,
44            timestamp: None,
45            tool_use_result: None,
46            subagent_type: None,
47            task_description: None,
48            origin: None,
49            priority: None,
50            is_synthetic: None,
51            should_query: None,
52            is_meta: None,
53            is_visible_in_transcript_only: None,
54            is_virtual: None,
55            is_compact_summary: None,
56            summarize_metadata: None,
57            mcp_meta: None,
58            source_tool_use_id: None,
59            source_tool_assistant_uuid: None,
60            image_paste_ids: None,
61            client_platform: None,
62            inbound_origin: None,
63            is_replay: None,
64            file_attachments: None,
65        })
66    }
67
68    /// Create a user message with content blocks
69    pub fn user_message_blocks(blocks: Vec<ContentBlock>, session_id: Uuid) -> Self {
70        ClaudeInput::User(UserMessage {
71            message: MessageContent {
72                role: super::MessageRole::User,
73                content: blocks,
74            },
75            session_id: Some(session_id),
76            parent_tool_use_id: None,
77            uuid: None,
78            timestamp: None,
79            tool_use_result: None,
80            subagent_type: None,
81            task_description: None,
82            origin: None,
83            priority: None,
84            is_synthetic: None,
85            should_query: None,
86            is_meta: None,
87            is_visible_in_transcript_only: None,
88            is_virtual: None,
89            is_compact_summary: None,
90            summarize_metadata: None,
91            mcp_meta: None,
92            source_tool_use_id: None,
93            source_tool_assistant_uuid: None,
94            image_paste_ids: None,
95            client_platform: None,
96            inbound_origin: None,
97            is_replay: None,
98            file_attachments: None,
99        })
100    }
101
102    /// Create an interrupt control message.
103    ///
104    /// Sends `{ "subtype": "interrupt" }` to the CLI subprocess's stdin,
105    /// telling Claude to stop its current response and return control
106    /// without killing the session.
107    pub fn interrupt() -> Self {
108        ClaudeInput::Raw(serde_json::to_value(super::SDKControlInterruptRequest::new()).unwrap())
109    }
110
111    /// Create a user message with an image and optional text
112    /// Only supports JPEG, PNG, GIF, and WebP media types
113    pub fn user_message_with_image(
114        image_data: String,
115        media_type: super::MediaType,
116        text: Option<String>,
117        session_id: Uuid,
118    ) -> Result<Self, String> {
119        // Validate media type
120        match &media_type {
121            super::MediaType::Jpeg
122            | super::MediaType::Png
123            | super::MediaType::Gif
124            | super::MediaType::Webp => {}
125            other => {
126                return Err(format!(
127                    "Invalid media type '{}'. Only JPEG, PNG, GIF, and WebP are supported.",
128                    other
129                ));
130            }
131        }
132
133        let mut blocks = vec![ContentBlock::Image(ImageBlock {
134            source: ImageSource {
135                source_type: super::ImageSourceType::Base64,
136                media_type,
137                data: image_data,
138            },
139        })];
140
141        if let Some(text_content) = text {
142            blocks.push(ContentBlock::Text(TextBlock {
143                text: text_content,
144                citations: Vec::new(),
145            }));
146        }
147
148        Ok(Self::user_message_blocks(blocks, session_id))
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn test_serialize_user_message() {
158        let session_uuid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
159        let input = ClaudeInput::user_message("Hello, Claude!", session_uuid);
160        let json = serde_json::to_string(&input).unwrap();
161        assert!(json.contains("\"type\":\"user\""));
162        assert!(json.contains("\"role\":\"user\""));
163        assert!(json.contains("\"text\":\"Hello, Claude!\""));
164        assert!(json.contains("550e8400-e29b-41d4-a716-446655440000"));
165    }
166}