claude_session_types/events/attachment.rs
1//! Attachment types (Level 4)
2//!
3//! Found in `.data.normalizedMessages[].attachment` within progress events.
4//!
5//! # Attachment Types
6//!
7//! ```text
8//! Attachments (.attachment.type)
9//! ├── hook_success (31,648) - Successful hook execution
10//! ├── todo_reminder (2,914) - Todo list reminders
11//! ├── critical_system_reminder (1,096) - Critical warnings
12//! ├── edited_text_file (302) - File edit summaries
13//! ├── edited_notebook_cell - Jupyter cell edits
14//! ├── file_snapshot - File state snapshots
15//! ├── hook_failure - Failed hook execution
16//! ├── hook_progress - Hook execution progress
17//! ├── agent_spawn - Agent delegation info
18//! └── ... (more types as discovered)
19//! ```
20//!
21//! # Usage
22//!
23//! Attachments appear in progress events as part of normalized messages:
24//!
25//! ```json
26//! {
27//! "type": "progress",
28//! "data": {
29//! "normalizedMessages": [
30//! {
31//! "type": "attachment",
32//! "attachment": {
33//! "type": "hook_success",
34//! "hookName": "pre-commit",
35//! "output": "✓ All checks passed"
36//! }
37//! }
38//! ]
39//! }
40//! }
41//! ```
42
43use serde::{Deserialize, Serialize};
44use serde_json::Value as JsonValue;
45
46/// Attachment block wrapper
47///
48/// Contains an attachment of a specific type.
49///
50/// Found in: `progress.data.normalizedMessages[].attachment`
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct AttachmentBlock {
53 /// Attachment type and data
54 #[serde(flatten)]
55 pub attachment: AttachmentType,
56}
57
58/// Attachment type discriminator
59///
60/// All possible attachment types found in progress normalized messages.
61///
62/// # Frequency (per large session)
63///
64/// - `HookSuccess`: ~31k (most common)
65/// - `TodoReminder`: ~2.9k
66/// - `CriticalSystemReminder`: ~1.1k
67/// - `EditedTextFile`: ~302
68/// - Others: rare
69#[derive(Debug, Clone, Serialize, Deserialize)]
70#[serde(tag = "type", rename_all = "snake_case")]
71pub enum AttachmentType {
72 /// Hook execution success
73 ///
74 /// Emitted when a pre/post hook completes successfully.
75 ///
76 /// # Example
77 ///
78 /// ```json
79 /// {
80 /// "type": "hook_success",
81 /// "hookName": "pre-commit",
82 /// "hookEvent": "pre-tool-use",
83 /// "output": "✓ All checks passed"
84 /// }
85 /// ```
86 ///
87 /// # Frequency
88 ///
89 /// ~31,648 occurrences per large session (most common attachment)
90 HookSuccess(HookSuccess),
91
92 /// Hook execution failure
93 ///
94 /// Emitted when a pre/post hook fails.
95 ///
96 /// # Example
97 ///
98 /// ```json
99 /// {
100 /// "type": "hook_failure",
101 /// "hookName": "pre-commit",
102 /// "hookEvent": "pre-tool-use",
103 /// "error": "Tests failed",
104 /// "exitCode": 1
105 /// }
106 /// ```
107 HookFailure(HookFailure),
108
109 /// Hook execution progress
110 ///
111 /// Real-time progress updates during hook execution.
112 ///
113 /// # Example
114 ///
115 /// ```json
116 /// {
117 /// "type": "hook_progress",
118 /// "hookName": "pre-commit",
119 /// "hookEvent": "pre-tool-use",
120 /// "output": "Running tests..."
121 /// }
122 /// ```
123 HookProgress(HookProgress),
124
125 /// Todo list reminder
126 ///
127 /// Reminds Claude of current todo items.
128 ///
129 /// # Example
130 ///
131 /// ```json
132 /// {
133 /// "type": "todo_reminder",
134 /// "todos": [
135 /// {"content": "Fix bug in parser", "status": "in_progress", "activeForm": "Fixing bug"},
136 /// {"content": "Write tests", "status": "pending", "activeForm": "Writing tests"}
137 /// ]
138 /// }
139 /// ```
140 ///
141 /// # Frequency
142 ///
143 /// ~2,914 occurrences per large session
144 TodoReminder(TodoReminder),
145
146 /// Critical system reminder
147 ///
148 /// Important system warnings or reminders.
149 ///
150 /// # Example
151 ///
152 /// ```json
153 /// {
154 /// "type": "critical_system_reminder",
155 /// "message": "Budget warning: 80% of tokens used",
156 /// "level": "warning"
157 /// }
158 /// ```
159 ///
160 /// # Frequency
161 ///
162 /// ~1,096 occurrences per large session
163 CriticalSystemReminder(CriticalSystemReminder),
164
165 /// Edited text file summary
166 ///
167 /// Summary of file edits with line-numbered snippet.
168 ///
169 /// # Example
170 ///
171 /// ```json
172 /// {
173 /// "type": "edited_text_file",
174 /// "filename": "/path/to/file.rs",
175 /// "snippet": "42→pub mod kucoin;\n43→pub mod binance;...",
176 /// "description": "Added new exchange modules"
177 /// }
178 /// ```
179 ///
180 /// # Frequency
181 ///
182 /// ~302 occurrences per large session
183 EditedTextFile(EditedTextFile),
184
185 /// Edited notebook cell
186 ///
187 /// Summary of Jupyter notebook cell edits.
188 ///
189 /// # Example
190 ///
191 /// ```json
192 /// {
193 /// "type": "edited_notebook_cell",
194 /// "filename": "/path/to/notebook.ipynb",
195 /// "cellIndex": 5,
196 /// "cellType": "code",
197 /// "snippet": "import pandas as pd..."
198 /// }
199 /// ```
200 EditedNotebookCell(EditedNotebookCell),
201
202 /// File snapshot
203 ///
204 /// Snapshot of file state at a point in time.
205 ///
206 /// # Example
207 ///
208 /// ```json
209 /// {
210 /// "type": "file_snapshot",
211 /// "filePath": "/path/to/file.rs",
212 /// "content": "pub fn main() {}",
213 /// "timestamp": "2024-01-01T00:00:00Z"
214 /// }
215 /// ```
216 FileSnapshot(FileSnapshot),
217
218 /// Agent spawn notification
219 ///
220 /// Notifies that an agent was spawned for a task.
221 ///
222 /// # Example
223 ///
224 /// ```json
225 /// {
226 /// "type": "agent_spawn",
227 /// "agentId": "abc123",
228 /// "agentSlug": "rust-implementer",
229 /// "prompt": "Implement feature X"
230 /// }
231 /// ```
232 AgentSpawn(AgentSpawn),
233
234 /// Unknown attachment type (forward compatibility)
235 #[serde(other)]
236 Unknown,
237}
238
239impl AttachmentType {
240 /// Snake-case name of this variant, matching the wire `type` tag (e.g.
241 /// `"hook_success"`, `"todo_reminder"`).
242 #[must_use]
243 pub fn type_name(&self) -> &'static str {
244 match self {
245 Self::HookSuccess(_) => "hook_success",
246 Self::HookFailure(_) => "hook_failure",
247 Self::HookProgress(_) => "hook_progress",
248 Self::TodoReminder(_) => "todo_reminder",
249 Self::CriticalSystemReminder(_) => "critical_system_reminder",
250 Self::EditedTextFile(_) => "edited_text_file",
251 Self::EditedNotebookCell(_) => "edited_notebook_cell",
252 Self::FileSnapshot(_) => "file_snapshot",
253 Self::AgentSpawn(_) => "agent_spawn",
254 Self::Unknown => "unknown",
255 }
256 }
257}
258
259/// Hook execution success
260///
261/// Most common attachment type (~31k per session).
262#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct HookSuccess {
264 /// Hook name (e.g., "pre-commit")
265 #[serde(rename = "hookName")]
266 pub hook_name: String,
267
268 /// Hook event type (e.g., "pre-tool-use", "post-tool-use")
269 #[serde(rename = "hookEvent")]
270 pub hook_event: String,
271
272 /// Hook output
273 pub output: Option<String>,
274
275 /// Execution time (milliseconds)
276 #[serde(rename = "executionTimeMs")]
277 pub execution_time_ms: Option<u64>,
278}
279
280/// Hook execution failure
281#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct HookFailure {
283 /// Hook name
284 #[serde(rename = "hookName")]
285 pub hook_name: String,
286
287 /// Hook event type
288 #[serde(rename = "hookEvent")]
289 pub hook_event: String,
290
291 /// Error message
292 pub error: String,
293
294 /// Exit code
295 #[serde(rename = "exitCode")]
296 pub exit_code: Option<i32>,
297
298 /// Error output (stderr)
299 pub stderr: Option<String>,
300}
301
302/// Hook execution progress
303#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct HookProgress {
305 /// Hook name
306 #[serde(rename = "hookName")]
307 pub hook_name: String,
308
309 /// Hook event type
310 #[serde(rename = "hookEvent")]
311 pub hook_event: String,
312
313 /// Progress output
314 pub output: String,
315}
316
317/// Todo list reminder
318///
319/// ~2,914 occurrences per large session.
320#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct TodoReminder {
322 /// Todo items
323 pub todos: Vec<TodoItem>,
324}
325
326/// Todo item
327#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct TodoItem {
329 /// Task description (imperative form)
330 pub content: String,
331
332 /// Active form (present continuous for display)
333 #[serde(rename = "activeForm")]
334 pub active_form: String,
335
336 /// Task status: "pending", "`in_progress`", "completed"
337 pub status: String,
338}
339
340/// Critical system reminder
341///
342/// ~1,096 occurrences per large session.
343#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct CriticalSystemReminder {
345 /// Reminder message
346 pub message: String,
347
348 /// Severity level: "info", "warning", "error"
349 pub level: Option<String>,
350
351 /// Additional context
352 #[serde(flatten)]
353 pub extra: JsonValue,
354}
355
356/// Edited text file summary
357///
358/// ~302 occurrences per large session.
359#[derive(Debug, Clone, Serialize, Deserialize)]
360pub struct EditedTextFile {
361 /// File path
362 pub filename: String,
363
364 /// Line-numbered snippet showing changes
365 ///
366 /// Format: "42→pub mod kucoin;\n43→pub mod binance;..."
367 pub snippet: String,
368
369 /// Description of changes
370 pub description: Option<String>,
371}
372
373/// Edited notebook cell
374#[derive(Debug, Clone, Serialize, Deserialize)]
375pub struct EditedNotebookCell {
376 /// Notebook file path
377 pub filename: String,
378
379 /// Cell index (0-based)
380 #[serde(rename = "cellIndex")]
381 pub cell_index: u64,
382
383 /// Cell type: "code", "markdown", "raw"
384 #[serde(rename = "cellType")]
385 pub cell_type: String,
386
387 /// Cell content snippet
388 pub snippet: String,
389
390 /// Description of changes
391 pub description: Option<String>,
392}
393
394/// File snapshot
395#[derive(Debug, Clone, Serialize, Deserialize)]
396pub struct FileSnapshot {
397 /// File path
398 #[serde(rename = "filePath")]
399 pub file_path: String,
400
401 /// File contents at snapshot time
402 pub content: String,
403
404 /// Snapshot timestamp
405 pub timestamp: Option<String>,
406}
407
408/// Agent spawn notification
409#[derive(Debug, Clone, Serialize, Deserialize)]
410pub struct AgentSpawn {
411 /// Agent ID
412 #[serde(rename = "agentId")]
413 pub agent_id: String,
414
415 /// Agent slug (e.g., "rust-implementer")
416 #[serde(rename = "agentSlug")]
417 pub agent_slug: String,
418
419 /// Agent prompt/task
420 pub prompt: String,
421}
422
423#[cfg(test)]
424mod tests {
425 use super::*;
426
427 #[test]
428 fn test_parse_hook_success() {
429 let json = r#"{
430 "type": "hook_success",
431 "hookName": "pre-commit",
432 "hookEvent": "pre-tool-use",
433 "output": "✓ All checks passed",
434 "executionTimeMs": 150
435 }"#;
436
437 let attachment: AttachmentType = serde_json::from_str(json).unwrap();
438 assert!(matches!(attachment, AttachmentType::HookSuccess(_)));
439
440 if let AttachmentType::HookSuccess(hook) = attachment {
441 assert_eq!(hook.hook_name, "pre-commit");
442 assert_eq!(hook.hook_event, "pre-tool-use");
443 assert_eq!(hook.output, Some("✓ All checks passed".to_string()));
444 assert_eq!(hook.execution_time_ms, Some(150));
445 }
446 }
447
448 #[test]
449 fn test_parse_todo_reminder() {
450 let json = r#"{
451 "type": "todo_reminder",
452 "todos": [
453 {
454 "content": "Fix bug",
455 "activeForm": "Fixing bug",
456 "status": "in_progress"
457 },
458 {
459 "content": "Write tests",
460 "activeForm": "Writing tests",
461 "status": "pending"
462 }
463 ]
464 }"#;
465
466 let attachment: AttachmentType = serde_json::from_str(json).unwrap();
467 assert!(matches!(attachment, AttachmentType::TodoReminder(_)));
468
469 if let AttachmentType::TodoReminder(reminder) = attachment {
470 assert_eq!(reminder.todos.len(), 2);
471 assert_eq!(reminder.todos[0].content, "Fix bug");
472 assert_eq!(reminder.todos[0].status, "in_progress");
473 assert_eq!(reminder.todos[1].content, "Write tests");
474 assert_eq!(reminder.todos[1].status, "pending");
475 }
476 }
477
478 #[test]
479 fn test_parse_edited_text_file() {
480 let json = r#"{
481 "type": "edited_text_file",
482 "filename": "/path/to/file.rs",
483 "snippet": "42→pub mod kucoin;\n43→pub mod binance;",
484 "description": "Added exchange modules"
485 }"#;
486
487 let attachment: AttachmentType = serde_json::from_str(json).unwrap();
488 assert!(matches!(attachment, AttachmentType::EditedTextFile(_)));
489
490 if let AttachmentType::EditedTextFile(edited) = attachment {
491 assert_eq!(edited.filename, "/path/to/file.rs");
492 assert!(edited.snippet.contains("pub mod kucoin"));
493 assert_eq!(
494 edited.description,
495 Some("Added exchange modules".to_string())
496 );
497 }
498 }
499
500 #[test]
501 fn test_parse_critical_reminder() {
502 let json = r#"{
503 "type": "critical_system_reminder",
504 "message": "Budget warning: 80% used",
505 "level": "warning"
506 }"#;
507
508 let attachment: AttachmentType = serde_json::from_str(json).unwrap();
509 assert!(matches!(
510 attachment,
511 AttachmentType::CriticalSystemReminder(_)
512 ));
513
514 if let AttachmentType::CriticalSystemReminder(reminder) = attachment {
515 assert_eq!(reminder.message, "Budget warning: 80% used");
516 assert_eq!(reminder.level, Some("warning".to_string()));
517 }
518 }
519
520 #[test]
521 fn test_parse_agent_spawn() {
522 let json = r#"{
523 "type": "agent_spawn",
524 "agentId": "abc123",
525 "agentSlug": "rust-implementer",
526 "prompt": "Implement feature X"
527 }"#;
528
529 let attachment: AttachmentType = serde_json::from_str(json).unwrap();
530 assert!(matches!(attachment, AttachmentType::AgentSpawn(_)));
531
532 if let AttachmentType::AgentSpawn(spawn) = attachment {
533 assert_eq!(spawn.agent_id, "abc123");
534 assert_eq!(spawn.agent_slug, "rust-implementer");
535 assert_eq!(spawn.prompt, "Implement feature X");
536 }
537 }
538}