1#![allow(
6 missing_docs,
7 reason = "fields mirror pi's TypeScript types; contracts are noted where they differ from the name"
8)]
9
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13use crate::message::{Content, Message, Usage};
14use crate::present;
15
16pub const CURRENT_VERSION: u32 = 3;
18
19#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
21#[serde(tag = "type", rename_all = "snake_case")]
22pub enum FileEntry {
23 Session(SessionHeader),
25 Message(MessageEntry),
26 ThinkingLevelChange(ThinkingLevelChangeEntry),
27 ModelChange(ModelChangeEntry),
28 Usage(UsageEntry),
29 Compaction(CompactionEntry),
30 BranchSummary(BranchSummaryEntry),
31 Custom(CustomEntry),
33 CustomMessage(CustomMessageEntry),
35 ContextEdit(ContextEditEntry),
36 Label(LabelEntry),
37 SessionInfo(SessionInfoEntry),
38}
39
40#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
41#[serde(rename_all = "camelCase")]
42pub struct SessionHeader {
43 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub version: Option<u32>,
46 pub id: String,
47 pub timestamp: String,
49 pub cwd: String,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub parent_session: Option<String>,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub provider: Option<String>,
56 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub model_id: Option<String>,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub thinking_level: Option<String>,
62 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub branched_from: Option<String>,
65}
66
67#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
69#[serde(rename_all = "camelCase")]
70pub struct EntryMeta {
71 pub id: String,
73 pub parent_id: Option<String>,
75 pub timestamp: String,
77}
78
79#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
80pub struct MessageEntry {
81 #[serde(flatten)]
82 pub meta: EntryMeta,
83 pub message: Message,
84}
85
86#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
87#[serde(rename_all = "camelCase")]
88pub struct ThinkingLevelChangeEntry {
89 #[serde(flatten)]
90 pub meta: EntryMeta,
91 pub thinking_level: String,
92}
93
94#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
95#[serde(rename_all = "camelCase")]
96pub struct ModelChangeEntry {
97 #[serde(flatten)]
98 pub meta: EntryMeta,
99 pub provider: String,
100 pub model_id: String,
101}
102
103#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
105pub struct UsageEntry {
106 #[serde(flatten)]
107 pub meta: EntryMeta,
108 pub kind: String,
109 pub provider: String,
110 pub model: String,
111 pub usage: Usage,
112 #[serde(default, skip_serializing_if = "Option::is_none")]
113 pub note: Option<String>,
114}
115
116#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
117#[serde(rename_all = "camelCase")]
118pub struct CompactionEntry {
119 #[serde(flatten)]
120 pub meta: EntryMeta,
121 pub summary: String,
122 #[serde(
125 default,
126 deserialize_with = "present",
127 skip_serializing_if = "Option::is_none"
128 )]
129 pub first_kept_entry_id: Option<Option<String>>,
130 pub tokens_before: u64,
131 #[serde(
132 default,
133 deserialize_with = "present",
134 skip_serializing_if = "Option::is_none"
135 )]
136 pub details: Option<Value>,
137 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub usage: Option<Usage>,
139 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub from_hook: Option<bool>,
142 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub system_message: Option<Box<Message>>,
145}
146
147#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
148#[serde(rename_all = "camelCase")]
149pub struct BranchSummaryEntry {
150 #[serde(flatten)]
151 pub meta: EntryMeta,
152 pub from_id: String,
153 pub summary: String,
154 #[serde(
155 default,
156 deserialize_with = "present",
157 skip_serializing_if = "Option::is_none"
158 )]
159 pub details: Option<Value>,
160 #[serde(default, skip_serializing_if = "Option::is_none")]
161 pub usage: Option<Usage>,
162 #[serde(default, skip_serializing_if = "Option::is_none")]
163 pub from_hook: Option<bool>,
164}
165
166#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
168#[serde(rename_all = "camelCase")]
169pub struct CustomEntry {
170 pub custom_type: String,
171 #[serde(
172 default,
173 deserialize_with = "present",
174 skip_serializing_if = "Option::is_none"
175 )]
176 pub data: Option<Value>,
177 #[serde(flatten)]
178 pub meta: EntryMeta,
179}
180
181#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
183#[serde(rename_all = "camelCase")]
184pub struct CustomMessageEntry {
185 pub custom_type: String,
186 pub content: Content,
187 pub display: bool,
188 #[serde(
189 default,
190 deserialize_with = "present",
191 skip_serializing_if = "Option::is_none"
192 )]
193 pub details: Option<Value>,
194 #[serde(flatten)]
195 pub meta: EntryMeta,
196}
197
198#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
200#[serde(rename_all = "camelCase")]
201pub struct ContextEditEntry {
202 #[serde(flatten)]
203 pub meta: EntryMeta,
204 pub target_id: String,
205 pub replacement: Option<Replacement>,
207}
208
209#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
210pub struct Replacement {
211 pub content: Content,
212}
213
214#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
215#[serde(rename_all = "camelCase")]
216pub struct LabelEntry {
217 #[serde(flatten)]
218 pub meta: EntryMeta,
219 pub target_id: String,
220 #[serde(default, skip_serializing_if = "Option::is_none")]
222 pub label: Option<String>,
223}
224
225#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
226pub struct SessionInfoEntry {
227 #[serde(flatten)]
228 pub meta: EntryMeta,
229 #[serde(default, skip_serializing_if = "Option::is_none")]
230 pub name: Option<String>,
231}
232
233impl FileEntry {
234 pub fn meta(&self) -> Option<&EntryMeta> {
236 Some(match self {
237 FileEntry::Session(_) => return None,
238 FileEntry::Message(entry) => &entry.meta,
239 FileEntry::ThinkingLevelChange(entry) => &entry.meta,
240 FileEntry::ModelChange(entry) => &entry.meta,
241 FileEntry::Usage(entry) => &entry.meta,
242 FileEntry::Compaction(entry) => &entry.meta,
243 FileEntry::BranchSummary(entry) => &entry.meta,
244 FileEntry::Custom(entry) => &entry.meta,
245 FileEntry::CustomMessage(entry) => &entry.meta,
246 FileEntry::ContextEdit(entry) => &entry.meta,
247 FileEntry::Label(entry) => &entry.meta,
248 FileEntry::SessionInfo(entry) => &entry.meta,
249 })
250 }
251}