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        Self::text_user_message(text, Some(session_id))
34    }
35
36    /// Create a simple text user message associated with the current CLI
37    /// process session.
38    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    /// Create a user message with content blocks
93    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    /// Create an interrupt control request.
141    ///
142    /// Serializes to the `control_request` envelope the CLI requires:
143    /// `{"type":"control_request","request_id":...,"request":{"subtype":"interrupt"}}`,
144    /// telling Claude to stop its current response and return control
145    /// without killing the session. The CLI acknowledges with a
146    /// `control_response` carrying the same `request_id`.
147    ///
148    /// `request_id` must be unique per request; the clients generate
149    /// `interrupt-<uuid>` ids.
150    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    /// Create an `apply_flag_settings` control request, e.g. to change the
158    /// session's effort level or toggle ultracode mid-session.
159    ///
160    /// Serializes to
161    /// `{"type":"control_request","request_id":...,"request":{"subtype":"apply_flag_settings","settings":{...}}}`.
162    /// The CLI answers with a `control_response` carrying the same
163    /// `request_id`. The clients generate `apply-flag-settings-<uuid>` ids.
164    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    /// Create a user message with an image and optional text
175    /// Only supports JPEG, PNG, GIF, and WebP media types
176    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        // Validate media type
183        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}