Skip to main content

opendev_models/
frontend_event.rs

1//! Shared frontend event types consumed by both TUI and Web UI.
2//!
3//! This module defines the canonical set of events that frontends render.
4//! The Web UI serializes these to JSON over WebSocket; the TUI will
5//! consume them directly once the incremental convergence from `AppEvent`
6//! is complete.
7//!
8//! All public types derive `ts_rs::TS` so TypeScript definitions can be
9//! auto-generated with `cargo test -p opendev-models export_frontend_types`.
10
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use ts_rs::TS;
14
15// ─── Top-Level Event Enum ───────────────────────────────────────────────────
16
17/// A frontend event that both the TUI and Web UI can render.
18///
19/// Serialized as a tagged union: `{"type": "MessageChunk", "data": {...}}`.
20#[derive(Debug, Clone, Serialize, Deserialize, TS)]
21#[serde(tag = "type", content = "data")]
22#[ts(export)]
23pub enum FrontendEvent {
24    // ── Message Lifecycle ────────────────────────────────────────────────
25    MessageStart(MessageStartPayload),
26    MessageChunk(MessageChunkPayload),
27    MessageComplete(MessageCompletePayload),
28
29    // ── Tool Events ─────────────────────────────────────────────────────
30    ToolCall(ToolCallPayload),
31    ToolResult(ToolResultPayload),
32
33    // ── Thinking / Reasoning ────────────────────────────────────────────
34    ThinkingBlock(ThinkingBlockPayload),
35
36    // ── Subagent Events ─────────────────────────────────────────────────
37    SubagentStarted(SubagentStartedPayload),
38    SubagentCompleted(SubagentCompletedPayload),
39    NestedToolCall(NestedToolCallPayload),
40    NestedToolResult(NestedToolResultPayload),
41
42    // ── Status & Progress ───────────────────────────────────────────────
43    StatusUpdate(StatusUpdatePayload),
44    Progress(ProgressPayload),
45
46    // ── Approval / Ask-User / Plan ──────────────────────────────────────
47    ApprovalRequired(ApprovalRequiredPayload),
48    AskUserRequired(AskUserRequiredPayload),
49    PlanApprovalRequired(PlanApprovalRequiredPayload),
50
51    // ── Session Activity ────────────────────────────────────────────────
52    SessionActivity(SessionActivityPayload),
53
54    // ── Errors ──────────────────────────────────────────────────────────
55    Error(ErrorPayload),
56}
57
58// ─── Payload Structs ────────────────────────────────────────────────────────
59
60#[derive(Debug, Clone, Serialize, Deserialize, TS)]
61#[ts(export)]
62pub struct MessageStartPayload {
63    pub session_id: String,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize, TS)]
67#[ts(export)]
68pub struct MessageChunkPayload {
69    pub session_id: String,
70    pub content: String,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize, TS)]
74#[ts(export)]
75pub struct MessageCompletePayload {
76    pub session_id: String,
77    pub role: String,
78    pub content: String,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize, TS)]
82#[ts(export)]
83pub struct ToolCallPayload {
84    pub session_id: String,
85    pub tool_id: String,
86    pub tool_name: String,
87    #[ts(type = "Record<string, any>")]
88    pub arguments: HashMap<String, serde_json::Value>,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize, TS)]
92#[ts(export)]
93pub struct ToolResultPayload {
94    pub session_id: String,
95    pub tool_id: String,
96    pub tool_name: String,
97    pub output: String,
98    pub success: bool,
99    /// Optional todo state included when tool is a todo-related tool.
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub todos: Option<Vec<TodoItemPayload>>,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize, TS)]
105#[ts(export)]
106pub struct TodoItemPayload {
107    pub id: String,
108    pub title: String,
109    pub status: String,
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub active_form: Option<String>,
112    #[serde(default)]
113    pub children: Vec<TodoChildPayload>,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize, TS)]
117#[ts(export)]
118pub struct TodoChildPayload {
119    pub title: String,
120    pub status: String,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize, TS)]
124#[ts(export)]
125pub struct ThinkingBlockPayload {
126    pub session_id: String,
127    pub content: String,
128    #[serde(default)]
129    pub block_start: bool,
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub level: Option<String>,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize, TS)]
135#[ts(export)]
136pub struct SubagentStartedPayload {
137    pub subagent_id: String,
138    pub agent_type: String,
139    pub description: String,
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub tool_call_id: Option<String>,
142    #[serde(skip_serializing_if = "Option::is_none")]
143    pub session_id: Option<String>,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize, TS)]
147#[ts(export)]
148pub struct SubagentCompletedPayload {
149    pub subagent_id: String,
150    pub success: bool,
151    pub result_summary: String,
152    pub tool_call_count: usize,
153    #[serde(skip_serializing_if = "Option::is_none")]
154    pub shallow_warning: Option<String>,
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub session_id: Option<String>,
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize, TS)]
160#[ts(export)]
161pub struct NestedToolCallPayload {
162    pub subagent_id: String,
163    pub tool_name: String,
164    pub tool_id: String,
165    pub arguments: HashMap<String, serde_json::Value>,
166    #[serde(default = "default_depth")]
167    pub depth: u32,
168}
169
170fn default_depth() -> u32 {
171    1
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize, TS)]
175#[ts(export)]
176pub struct NestedToolResultPayload {
177    pub subagent_id: String,
178    pub tool_name: String,
179    pub tool_id: String,
180    pub success: bool,
181    #[serde(default = "default_depth")]
182    pub depth: u32,
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize, TS)]
186#[ts(export)]
187pub struct StatusUpdatePayload {
188    pub session_id: String,
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub model: Option<String>,
191    #[serde(skip_serializing_if = "Option::is_none")]
192    pub provider: Option<String>,
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub input_tokens: Option<u64>,
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub output_tokens: Option<u64>,
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub context_usage_pct: Option<f64>,
199    #[serde(skip_serializing_if = "Option::is_none")]
200    pub session_cost_usd: Option<f64>,
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub git_branch: Option<String>,
203    #[serde(skip_serializing_if = "Option::is_none")]
204    pub autonomy_level: Option<String>,
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub thinking_level: Option<String>,
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub file_changes: Option<FileChangesPayload>,
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub todos: Option<Vec<TodoItemPayload>>,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize, TS)]
214#[ts(export)]
215pub struct FileChangesPayload {
216    pub files: usize,
217    pub additions: u64,
218    pub deletions: u64,
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize, TS)]
222#[ts(export)]
223pub struct ProgressPayload {
224    pub session_id: String,
225    pub status: String,
226    pub message: String,
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize, TS)]
230#[ts(export)]
231pub struct ApprovalRequiredPayload {
232    pub id: String,
233    pub tool_name: String,
234    pub description: String,
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub command: Option<String>,
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub session_id: Option<String>,
239}
240
241#[derive(Debug, Clone, Serialize, Deserialize, TS)]
242#[ts(export)]
243pub struct AskUserRequiredPayload {
244    pub request_id: String,
245    pub question: String,
246    #[serde(default)]
247    pub options: Vec<String>,
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub default: Option<String>,
250    #[serde(skip_serializing_if = "Option::is_none")]
251    pub session_id: Option<String>,
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize, TS)]
255#[ts(export)]
256pub struct PlanApprovalRequiredPayload {
257    pub request_id: String,
258    pub plan_content: String,
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub session_id: Option<String>,
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize, TS)]
264#[ts(export)]
265pub struct SessionActivityPayload {
266    pub session_id: String,
267    pub running: bool,
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize, TS)]
271#[ts(export)]
272pub struct ErrorPayload {
273    pub message: String,
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub session_id: Option<String>,
276}
277
278// ─── TypeScript Export Test ─────────────────────────────────────────────────
279
280#[cfg(test)]
281#[path = "frontend_event_tests.rs"]
282mod tests;