Skip to main content

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}