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            tool_result_meta: None,
59            source_tool_use_id: None,
60            source_tool_assistant_uuid: None,
61            image_paste_ids: None,
62            client_platform: None,
63            inbound_origin: None,
64            is_replay: None,
65            file_attachments: None,
66        })
67    }
68
69    /// Create a user message with content blocks
70    pub fn user_message_blocks(blocks: Vec<ContentBlock>, session_id: Uuid) -> Self {
71        ClaudeInput::User(UserMessage {
72            message: MessageContent {
73                role: super::MessageRole::User,
74                content: blocks,
75            },
76            session_id: Some(session_id),
77            parent_tool_use_id: None,
78            uuid: None,
79            timestamp: None,
80            tool_use_result: None,
81            subagent_type: None,
82            task_description: None,
83            origin: None,
84            priority: None,
85            is_synthetic: None,
86            should_query: None,
87            is_meta: None,
88            is_visible_in_transcript_only: None,
89            is_virtual: None,
90            is_compact_summary: None,
91            summarize_metadata: None,
92            mcp_meta: None,
93            tool_result_meta: None,
94            source_tool_use_id: None,
95            source_tool_assistant_uuid: None,
96            image_paste_ids: None,
97            client_platform: None,
98            inbound_origin: None,
99            is_replay: None,
100            file_attachments: None,
101        })
102    }
103
104    /// Create an interrupt control request.
105    ///
106    /// Serializes to the `control_request` envelope the CLI requires:
107    /// `{"type":"control_request","request_id":...,"request":{"subtype":"interrupt"}}`,
108    /// telling Claude to stop its current response and return control
109    /// without killing the session. The CLI acknowledges with a
110    /// `control_response` carrying the same `request_id`.
111    ///
112    /// `request_id` must be unique per request; the clients generate
113    /// `interrupt-<uuid>` ids.
114    pub fn interrupt(request_id: impl Into<String>) -> Self {
115        ClaudeInput::ControlRequest(ControlRequest {
116            request_id: request_id.into(),
117            request: super::ControlRequestPayload::Interrupt,
118        })
119    }
120
121    /// Create a user message with an image and optional text
122    /// Only supports JPEG, PNG, GIF, and WebP media types
123    pub fn user_message_with_image(
124        image_data: String,
125        media_type: super::MediaType,
126        text: Option<String>,
127        session_id: Uuid,
128    ) -> Result<Self, String> {
129        // Validate media type
130        match &media_type {
131            super::MediaType::Jpeg
132            | super::MediaType::Png
133            | super::MediaType::Gif
134            | super::MediaType::Webp => {}
135            other => {
136                return Err(format!(
137                    "Invalid media type '{}'. Only JPEG, PNG, GIF, and WebP are supported.",
138                    other
139                ));
140            }
141        }
142
143        let mut blocks = vec![ContentBlock::Image(ImageBlock {
144            source: ImageSource {
145                source_type: super::ImageSourceType::Base64,
146                media_type,
147                data: image_data,
148            },
149        })];
150
151        if let Some(text_content) = text {
152            blocks.push(ContentBlock::Text(TextBlock {
153                text: text_content,
154                citations: Vec::new(),
155            }));
156        }
157
158        Ok(Self::user_message_blocks(blocks, session_id))
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn test_interrupt_serializes_to_control_request_envelope() {
168        let input = ClaudeInput::interrupt("interrupt-abc");
169        assert_eq!(
170            serde_json::to_value(&input).unwrap(),
171            serde_json::json!({
172                "type": "control_request",
173                "request_id": "interrupt-abc",
174                "request": {"subtype": "interrupt"}
175            })
176        );
177    }
178
179    #[test]
180    fn test_serialize_user_message() {
181        let session_uuid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
182        let input = ClaudeInput::user_message("Hello, Claude!", session_uuid);
183        let json = serde_json::to_string(&input).unwrap();
184        assert!(json.contains("\"type\":\"user\""));
185        assert!(json.contains("\"role\":\"user\""));
186        assert!(json.contains("\"text\":\"Hello, Claude!\""));
187        assert!(json.contains("550e8400-e29b-41d4-a716-446655440000"));
188    }
189}