Skip to main content

oby_core/
entry.rs

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    /// Live byte stream; the wrapper binds it to the matching oby-tee connection by tool_use_id.
24    LiveStream {
25        tool_use_id: String,
26    },
27    /// Pre-rendered structured diff (for Edit/Write — placeholder in v0.1).
28    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
57/// Serialize SystemTime as integer milliseconds since epoch for JSON wire stability.
58mod 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}