1use serde::{Deserialize, Serialize};
2use std::time::SystemTime;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
5pub struct DisplayEntry {
6 pub agent_key: String,
7 pub tool_use_id: String,
8 pub tool: String,
9 #[serde(with = "ts_millis")]
10 pub timestamp: SystemTime,
11 pub headline: String,
12 pub body: EntryBody,
13 pub status: EntryStatus,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
17#[serde(tag = "type", rename_all = "snake_case")]
18pub enum EntryBody {
19 None,
20 Text {
21 text: String,
22 },
23 LiveStream {
25 tool_use_id: String,
26 },
27 Diff {
29 hunks: Vec<DiffHunk>,
30 },
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct DiffHunk {
35 pub old_start: u32,
36 pub old_lines: Vec<String>,
37 pub new_start: u32,
38 pub new_lines: Vec<String>,
39}
40
41#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
42#[serde(rename_all = "snake_case")]
43pub enum EntryStatus {
44 Pending,
45 Ok,
46 Error,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct DisplayEntryUpdate {
51 pub tool_use_id: String,
52 pub status: EntryStatus,
53 #[serde(default)]
54 pub append_body: Option<EntryBody>,
55}
56
57mod ts_millis {
59 use serde::{Deserialize, Deserializer, Serialize, Serializer};
60 use std::time::{Duration, SystemTime, UNIX_EPOCH};
61
62 pub fn serialize<S: Serializer>(t: &SystemTime, s: S) -> Result<S::Ok, S::Error> {
63 let ms = t
64 .duration_since(UNIX_EPOCH)
65 .map(|d| d.as_millis())
66 .unwrap_or(0);
67 (ms as u64).serialize(s)
68 }
69
70 pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<SystemTime, D::Error> {
71 let ms = u64::deserialize(d)?;
72 Ok(UNIX_EPOCH + Duration::from_millis(ms))
73 }
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79
80 #[test]
81 fn entry_roundtrips_through_json() {
82 let entry = DisplayEntry {
83 agent_key: "main".into(),
84 tool_use_id: "toolu_01".into(),
85 tool: "bash".into(),
86 timestamp: SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1716840000),
87 headline: "echo MAIN_PROBE".into(),
88 body: EntryBody::LiveStream {
89 tool_use_id: "toolu_01".into(),
90 },
91 status: EntryStatus::Pending,
92 };
93 let json = serde_json::to_string(&entry).unwrap();
94 let parsed: DisplayEntry = serde_json::from_str(&json).unwrap();
95 assert_eq!(parsed.agent_key, "main");
96 assert_eq!(parsed.status, EntryStatus::Pending);
97 match parsed.body {
98 EntryBody::LiveStream { tool_use_id } => assert_eq!(tool_use_id, "toolu_01"),
99 _ => panic!("expected LiveStream"),
100 }
101 }
102
103 #[test]
104 fn update_roundtrips() {
105 let upd = DisplayEntryUpdate {
106 tool_use_id: "toolu_01".into(),
107 status: EntryStatus::Ok,
108 append_body: Some(EntryBody::Text {
109 text: "exit 0".into(),
110 }),
111 };
112 let json = serde_json::to_string(&upd).unwrap();
113 let parsed: DisplayEntryUpdate = serde_json::from_str(&json).unwrap();
114 assert_eq!(parsed.status, EntryStatus::Ok);
115 }
116}