1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4use ulid::Ulid;
5
6use crate::{AgentId, ProvenanceChain, SessionId, UnderlineStyle};
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct BlockId(pub String);
11
12impl BlockId {
13 pub fn new() -> Self {
14 Self(Ulid::new().to_string())
15 }
16}
17
18impl Default for BlockId {
19 fn default() -> Self {
20 Self::new()
21 }
22}
23
24impl std::fmt::Display for BlockId {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 write!(f, "{}", self.0)
27 }
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct Block {
35 pub id: BlockId,
36 pub kind: BlockKind,
37 pub parent_id: Option<BlockId>,
38 pub agent_id: Option<AgentId>,
39 pub session_id: SessionId,
40 pub created_at: DateTime<Utc>,
41 pub updated_at: DateTime<Utc>,
42 pub status: BlockStatus,
43 pub content: BlockContent,
44 pub provenance: ProvenanceChain,
45}
46
47impl Block {
48 pub fn new(kind: BlockKind, session_id: SessionId, content: BlockContent) -> Self {
49 let now = Utc::now();
50 Self {
51 id: BlockId::new(),
52 kind,
53 parent_id: None,
54 agent_id: None,
55 session_id,
56 created_at: now,
57 updated_at: now,
58 status: BlockStatus::Pending,
59 content,
60 provenance: ProvenanceChain::default(),
61 }
62 }
63
64 pub fn with_agent(mut self, agent_id: AgentId) -> Self {
65 self.agent_id = Some(agent_id);
66 self
67 }
68
69 pub fn with_parent(mut self, parent_id: BlockId) -> Self {
70 self.parent_id = Some(parent_id);
71 self
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
77pub enum BlockKind {
78 Human,
79 Agent,
80 System,
81 Tool,
82 Approval,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87pub enum BlockStatus {
88 Pending,
89 Running,
90 Completed,
91 Failed,
92 Cancelled,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
99#[serde(tag = "type", rename_all = "snake_case")]
100pub enum BlockContent {
101 ShellCommand {
103 input: String,
104 output: TerminalOutput,
105 exit_code: Option<i32>,
106 cwd: PathBuf,
107 duration_ms: Option<u64>,
108 },
109
110 AgentMessage {
112 role: MessageRole,
113 content_blocks: Vec<ContentBlock>,
114 },
115
116 ToolCall {
118 tool_name: String,
119 tool_use_id: String,
120 input: serde_json::Value,
121 output: Option<String>,
122 streaming_text: Option<String>,
123 error: Option<String>,
124 collapsed_default: bool,
125 },
126
127 ApprovalRequest {
129 action: AgentAction,
130 reasoning: Option<String>,
131 granted: Option<bool>,
132 granter: Option<ActorId>,
133 },
134
135 FileEdit {
137 path: PathBuf,
138 diff: UnifiedDiff,
139 applied: bool,
140 },
141
142 PlanNode {
144 description: String,
145 subtask_ids: Vec<BlockId>,
146 progress: f32,
147 is_complete: bool,
148 },
149
150 Text { text: String },
152}
153
154#[derive(Debug, Clone, Default, Serialize, Deserialize)]
156pub struct TerminalOutput {
157 pub rows: Vec<TerminalRow>,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct TerminalRow {
163 pub cells: Vec<TerminalCell>,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct TerminalCell {
168 #[serde(
169 default,
170 alias = "character",
171 deserialize_with = "deser_grapheme_compat"
172 )]
173 pub grapheme: String,
174 pub fg: Option<Color>,
175 pub bg: Option<Color>,
176 pub bold: bool,
177 pub italic: bool,
178 #[serde(default, deserialize_with = "deser_underline_compat")]
179 pub underline: UnderlineStyle,
180 #[serde(default)]
181 pub strikethrough: bool,
182 #[serde(default, skip_serializing_if = "Option::is_none")]
183 pub link: Option<String>,
184}
185
186fn deser_grapheme_compat<'de, D>(deserializer: D) -> Result<String, D::Error>
188where
189 D: serde::Deserializer<'de>,
190{
191 use serde::Deserialize;
192 #[derive(Deserialize)]
193 #[serde(untagged)]
194 enum Either {
195 Ch(char),
196 Str(String),
197 }
198 match Either::deserialize(deserializer)? {
199 Either::Ch(c) => Ok(c.to_string()),
200 Either::Str(s) => Ok(s),
201 }
202}
203
204fn deser_underline_compat<'de, D>(deserializer: D) -> Result<UnderlineStyle, D::Error>
207where
208 D: serde::Deserializer<'de>,
209{
210 use serde::Deserialize;
211 #[derive(Deserialize)]
212 #[serde(untagged)]
213 enum Either {
214 Bool(bool),
215 Style(UnderlineStyle),
216 }
217 match Either::deserialize(deserializer)? {
218 Either::Bool(true) => Ok(UnderlineStyle::Single),
219 Either::Bool(false) => Ok(UnderlineStyle::None),
220 Either::Style(s) => Ok(s),
221 }
222}
223
224#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
226pub struct Color {
227 pub r: u8,
228 pub g: u8,
229 pub b: u8,
230}
231
232impl Color {
233 pub const WHITE: Self = Self {
234 r: 255,
235 g: 255,
236 b: 255,
237 };
238 pub const BLACK: Self = Self { r: 0, g: 0, b: 0 };
239 pub const GREEN: Self = Self {
240 r: 80,
241 g: 200,
242 b: 120,
243 };
244 pub const RED: Self = Self {
245 r: 220,
246 g: 70,
247 b: 70,
248 };
249 pub const BLUE: Self = Self {
250 r: 80,
251 g: 140,
252 b: 220,
253 };
254 pub const YELLOW: Self = Self {
255 r: 220,
256 g: 200,
257 b: 60,
258 };
259 pub const CYAN: Self = Self {
260 r: 80,
261 g: 200,
262 b: 200,
263 };
264 pub const GRAY: Self = Self {
265 r: 150,
266 g: 150,
267 b: 150,
268 };
269
270 pub fn to_wgpu(&self) -> [f32; 3] {
271 [
272 self.r as f32 / 255.0,
273 self.g as f32 / 255.0,
274 self.b as f32 / 255.0,
275 ]
276 }
277}
278
279#[derive(Debug, Clone, Serialize, Deserialize)]
281#[serde(tag = "type", rename_all = "snake_case")]
282pub enum ContentBlock {
283 Text {
284 text: String,
285 },
286 Code {
287 language: Option<String>,
288 code: String,
289 },
290 Thinking {
291 thinking: String,
292 },
293}
294
295#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
296pub enum MessageRole {
297 User,
298 Assistant,
299 System,
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize)]
304#[serde(tag = "kind", rename_all = "snake_case")]
305pub enum AgentAction {
306 FileWrite {
307 path: PathBuf,
308 content_preview: Option<String>,
309 },
310 FileRead {
311 path: PathBuf,
312 },
313 FileDelete {
314 path: PathBuf,
315 },
316 ShellExecute {
317 command: String,
318 },
319 NetworkRequest {
320 url: String,
321 method: String,
322 },
323 AgentSpawn {
324 agent_name: String,
325 },
326 ToolUse {
327 tool_name: String,
328 },
329}
330
331#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct UnifiedDiff {
334 pub old_path: Option<PathBuf>,
335 pub new_path: Option<PathBuf>,
336 pub hunks: Vec<DiffHunk>,
337}
338
339#[derive(Debug, Clone, Serialize, Deserialize)]
340pub struct DiffHunk {
341 pub old_start: u32,
342 pub old_lines: u32,
343 pub new_start: u32,
344 pub new_lines: u32,
345 pub lines: Vec<DiffLine>,
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize)]
349pub struct DiffLine {
350 pub kind: DiffLineKind,
351 pub content: String,
352}
353
354#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
355pub enum DiffLineKind {
356 Context,
357 Added,
358 Removed,
359}
360
361#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
363#[serde(tag = "kind", rename_all = "snake_case")]
364pub enum ActorId {
365 Human,
366 Agent { id: AgentId },
367}