use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AttachmentBlock {
#[serde(flatten)]
pub attachment: AttachmentType,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AttachmentType {
HookSuccess(HookSuccess),
HookFailure(HookFailure),
HookProgress(HookProgress),
TodoReminder(TodoReminder),
CriticalSystemReminder(CriticalSystemReminder),
EditedTextFile(EditedTextFile),
EditedNotebookCell(EditedNotebookCell),
FileSnapshot(FileSnapshot),
AgentSpawn(AgentSpawn),
#[serde(other)]
Unknown,
}
impl AttachmentType {
#[must_use]
pub fn type_name(&self) -> &'static str {
match self {
Self::HookSuccess(_) => "hook_success",
Self::HookFailure(_) => "hook_failure",
Self::HookProgress(_) => "hook_progress",
Self::TodoReminder(_) => "todo_reminder",
Self::CriticalSystemReminder(_) => "critical_system_reminder",
Self::EditedTextFile(_) => "edited_text_file",
Self::EditedNotebookCell(_) => "edited_notebook_cell",
Self::FileSnapshot(_) => "file_snapshot",
Self::AgentSpawn(_) => "agent_spawn",
Self::Unknown => "unknown",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HookSuccess {
#[serde(rename = "hookName")]
pub hook_name: String,
#[serde(rename = "hookEvent")]
pub hook_event: String,
pub output: Option<String>,
#[serde(rename = "executionTimeMs")]
pub execution_time_ms: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HookFailure {
#[serde(rename = "hookName")]
pub hook_name: String,
#[serde(rename = "hookEvent")]
pub hook_event: String,
pub error: String,
#[serde(rename = "exitCode")]
pub exit_code: Option<i32>,
pub stderr: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HookProgress {
#[serde(rename = "hookName")]
pub hook_name: String,
#[serde(rename = "hookEvent")]
pub hook_event: String,
pub output: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TodoReminder {
pub todos: Vec<TodoItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TodoItem {
pub content: String,
#[serde(rename = "activeForm")]
pub active_form: String,
pub status: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CriticalSystemReminder {
pub message: String,
pub level: Option<String>,
#[serde(flatten)]
pub extra: JsonValue,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EditedTextFile {
pub filename: String,
pub snippet: String,
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EditedNotebookCell {
pub filename: String,
#[serde(rename = "cellIndex")]
pub cell_index: u64,
#[serde(rename = "cellType")]
pub cell_type: String,
pub snippet: String,
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileSnapshot {
#[serde(rename = "filePath")]
pub file_path: String,
pub content: String,
pub timestamp: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentSpawn {
#[serde(rename = "agentId")]
pub agent_id: String,
#[serde(rename = "agentSlug")]
pub agent_slug: String,
pub prompt: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_hook_success() {
let json = r#"{
"type": "hook_success",
"hookName": "pre-commit",
"hookEvent": "pre-tool-use",
"output": "✓ All checks passed",
"executionTimeMs": 150
}"#;
let attachment: AttachmentType = serde_json::from_str(json).unwrap();
assert!(matches!(attachment, AttachmentType::HookSuccess(_)));
if let AttachmentType::HookSuccess(hook) = attachment {
assert_eq!(hook.hook_name, "pre-commit");
assert_eq!(hook.hook_event, "pre-tool-use");
assert_eq!(hook.output, Some("✓ All checks passed".to_string()));
assert_eq!(hook.execution_time_ms, Some(150));
}
}
#[test]
fn test_parse_todo_reminder() {
let json = r#"{
"type": "todo_reminder",
"todos": [
{
"content": "Fix bug",
"activeForm": "Fixing bug",
"status": "in_progress"
},
{
"content": "Write tests",
"activeForm": "Writing tests",
"status": "pending"
}
]
}"#;
let attachment: AttachmentType = serde_json::from_str(json).unwrap();
assert!(matches!(attachment, AttachmentType::TodoReminder(_)));
if let AttachmentType::TodoReminder(reminder) = attachment {
assert_eq!(reminder.todos.len(), 2);
assert_eq!(reminder.todos[0].content, "Fix bug");
assert_eq!(reminder.todos[0].status, "in_progress");
assert_eq!(reminder.todos[1].content, "Write tests");
assert_eq!(reminder.todos[1].status, "pending");
}
}
#[test]
fn test_parse_edited_text_file() {
let json = r#"{
"type": "edited_text_file",
"filename": "/path/to/file.rs",
"snippet": "42→pub mod kucoin;\n43→pub mod binance;",
"description": "Added exchange modules"
}"#;
let attachment: AttachmentType = serde_json::from_str(json).unwrap();
assert!(matches!(attachment, AttachmentType::EditedTextFile(_)));
if let AttachmentType::EditedTextFile(edited) = attachment {
assert_eq!(edited.filename, "/path/to/file.rs");
assert!(edited.snippet.contains("pub mod kucoin"));
assert_eq!(
edited.description,
Some("Added exchange modules".to_string())
);
}
}
#[test]
fn test_parse_critical_reminder() {
let json = r#"{
"type": "critical_system_reminder",
"message": "Budget warning: 80% used",
"level": "warning"
}"#;
let attachment: AttachmentType = serde_json::from_str(json).unwrap();
assert!(matches!(
attachment,
AttachmentType::CriticalSystemReminder(_)
));
if let AttachmentType::CriticalSystemReminder(reminder) = attachment {
assert_eq!(reminder.message, "Budget warning: 80% used");
assert_eq!(reminder.level, Some("warning".to_string()));
}
}
#[test]
fn test_parse_agent_spawn() {
let json = r#"{
"type": "agent_spawn",
"agentId": "abc123",
"agentSlug": "rust-implementer",
"prompt": "Implement feature X"
}"#;
let attachment: AttachmentType = serde_json::from_str(json).unwrap();
assert!(matches!(attachment, AttachmentType::AgentSpawn(_)));
if let AttachmentType::AgentSpawn(spawn) = attachment {
assert_eq!(spawn.agent_id, "abc123");
assert_eq!(spawn.agent_slug, "rust-implementer");
assert_eq!(spawn.prompt, "Implement feature X");
}
}
}