1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use serde_json::{Value, json};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct ApiError {
7 pub code: String,
8 pub message: String,
9}
10
11impl ApiError {
12 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
13 Self {
14 code: code.into(),
15 message: message.into(),
16 }
17 }
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21#[serde(rename_all = "camelCase")]
22pub struct RuntimeRecord {
23 pub runtime_id: String,
24 pub display_name: String,
25 pub codex_home: Option<String>,
26 pub codex_binary: String,
27 pub is_primary: bool,
28 pub auto_start: bool,
29 pub created_at_ms: i64,
30 pub updated_at_ms: i64,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34#[serde(rename_all = "camelCase")]
35pub struct RuntimeStatusSnapshot {
36 pub runtime_id: String,
37 pub status: String,
38 pub codex_home: Option<String>,
39 pub user_agent: Option<String>,
40 pub platform_family: Option<String>,
41 pub platform_os: Option<String>,
42 pub last_error: Option<String>,
43 pub pid: Option<u32>,
44 pub updated_at_ms: i64,
45}
46
47impl RuntimeStatusSnapshot {
48 pub fn stopped(runtime_id: impl Into<String>) -> Self {
49 Self {
50 runtime_id: runtime_id.into(),
51 status: "stopped".to_string(),
52 codex_home: None,
53 user_agent: None,
54 platform_family: None,
55 platform_os: None,
56 last_error: None,
57 pid: None,
58 updated_at_ms: now_millis(),
59 }
60 }
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
64#[serde(rename_all = "camelCase")]
65pub struct RuntimeSummary {
66 pub runtime_id: String,
67 pub display_name: String,
68 pub codex_home: Option<String>,
69 pub codex_binary: String,
70 pub is_primary: bool,
71 pub auto_start: bool,
72 pub created_at_ms: i64,
73 pub updated_at_ms: i64,
74 pub status: RuntimeStatusSnapshot,
75}
76
77impl RuntimeSummary {
78 pub fn from_parts(record: &RuntimeRecord, status: RuntimeStatusSnapshot) -> Self {
79 Self {
80 runtime_id: record.runtime_id.clone(),
81 display_name: record.display_name.clone(),
82 codex_home: record.codex_home.clone(),
83 codex_binary: record.codex_binary.clone(),
84 is_primary: record.is_primary,
85 auto_start: record.auto_start,
86 created_at_ms: record.created_at_ms,
87 updated_at_ms: record.updated_at_ms,
88 status,
89 }
90 }
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
94#[serde(rename_all = "camelCase")]
95pub struct DirectoryBookmarkRecord {
96 pub path: String,
97 pub display_name: String,
98 pub created_at_ms: i64,
99 pub updated_at_ms: i64,
100}
101
102#[derive(Debug, Clone, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct DirectoryHistoryRecord {
105 pub path: String,
106 pub display_name: String,
107 pub last_used_at_ms: i64,
108 pub use_count: i64,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
112#[serde(rename_all = "camelCase")]
113pub struct DirectoryEntry {
114 pub name: String,
115 pub path: String,
116 pub is_directory: bool,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
120#[serde(rename_all = "camelCase")]
121pub struct DirectoryListing {
122 pub path: String,
123 pub parent_path: Option<String>,
124 pub entries: Vec<DirectoryEntry>,
125}
126
127#[derive(Debug, Clone, Default, Serialize, Deserialize)]
128#[serde(rename_all = "camelCase")]
129#[serde(default)]
130pub struct ThreadStatusInfo {
131 pub kind: String,
132 pub reason: Option<String>,
133 pub raw: Value,
134}
135
136#[derive(Debug, Clone, Default, Serialize, Deserialize)]
137#[serde(rename_all = "camelCase")]
138#[serde(default)]
139pub struct ThreadTokenUsage {
140 pub input_tokens: Option<i64>,
141 pub cached_input_tokens: Option<i64>,
142 pub output_tokens: Option<i64>,
143 pub reasoning_tokens: Option<i64>,
144 pub total_tokens: Option<i64>,
145 pub raw: Value,
146 pub updated_at_ms: i64,
147}
148
149#[derive(Debug, Clone, Default, Serialize, Deserialize)]
150#[serde(rename_all = "camelCase")]
151#[serde(default)]
152pub struct ThreadSummary {
153 pub id: String,
154 pub runtime_id: String,
155 pub name: Option<String>,
156 pub note: Option<String>,
157 pub preview: String,
158 pub cwd: String,
159 pub status: String,
160 pub status_info: ThreadStatusInfo,
161 pub token_usage: Option<ThreadTokenUsage>,
162 pub model_provider: String,
163 pub source: String,
164 pub created_at: i64,
165 pub updated_at: i64,
166 pub is_loaded: bool,
167 pub is_active: bool,
168 pub archived: bool,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub struct TimelineEntry {
174 pub id: String,
175 pub runtime_id: String,
176 pub thread_id: String,
177 pub turn_id: Option<String>,
178 pub item_id: Option<String>,
179 pub entry_type: String,
180 pub title: Option<String>,
181 pub text: String,
182 pub status: Option<String>,
183 #[serde(default)]
184 pub metadata: Value,
185}
186
187#[derive(Debug, Clone, Default, Serialize, Deserialize)]
188#[serde(rename_all = "camelCase")]
189#[serde(default)]
190pub struct PendingServerRequestOption {
191 pub label: String,
192 pub description: Option<String>,
193 pub value: Option<Value>,
194 pub is_other: bool,
195 pub raw: Value,
196}
197
198#[derive(Debug, Clone, Default, Serialize, Deserialize)]
199#[serde(rename_all = "camelCase")]
200#[serde(default)]
201pub struct PendingServerRequestQuestion {
202 pub id: String,
203 pub header: Option<String>,
204 pub question: Option<String>,
205 pub required: bool,
206 pub options: Vec<PendingServerRequestOption>,
207 pub raw: Value,
208}
209
210#[derive(Debug, Clone, Default, Serialize, Deserialize)]
211#[serde(rename_all = "camelCase")]
212#[serde(default)]
213pub struct PendingServerRequestRecord {
214 pub request_id: String,
215 pub runtime_id: String,
216 pub rpc_request_id: Value,
217 pub request_type: String,
218 pub thread_id: Option<String>,
219 pub turn_id: Option<String>,
220 pub item_id: Option<String>,
221 pub title: Option<String>,
222 pub reason: Option<String>,
223 pub command: Option<String>,
224 pub cwd: Option<String>,
225 pub grant_root: Option<String>,
226 pub tool_name: Option<String>,
227 pub arguments: Option<Value>,
228 #[serde(default)]
229 pub questions: Vec<PendingServerRequestQuestion>,
230 pub proposed_execpolicy_amendment: Option<Value>,
231 pub network_approval_context: Option<Value>,
232 pub schema: Option<Value>,
233 #[serde(default)]
234 pub available_decisions: Vec<String>,
235 pub raw_payload: Value,
236 pub created_at_ms: i64,
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize)]
240#[serde(rename_all = "camelCase")]
241pub struct PersistedEvent {
242 pub seq: i64,
243 pub event_type: String,
244 pub runtime_id: Option<String>,
245 pub thread_id: Option<String>,
246 pub payload: Value,
247 pub created_at_ms: i64,
248}
249
250#[derive(Debug, Deserialize)]
251#[serde(tag = "kind", rename_all = "snake_case")]
252pub enum ClientEnvelope {
253 Hello {
254 device_id: String,
255 last_ack_seq: Option<i64>,
256 },
257 Request {
258 request_id: String,
259 action: String,
260 #[serde(default)]
261 payload: Value,
262 },
263 AckEvents {
264 last_seq: i64,
265 },
266 Ping,
267}
268
269#[derive(Debug, Serialize)]
270#[serde(tag = "kind", rename_all = "snake_case")]
271pub enum ServerEnvelope {
272 Hello {
273 bridge_version: String,
274 protocol_version: u32,
275 runtime: RuntimeStatusSnapshot,
276 runtimes: Vec<RuntimeSummary>,
277 directory_bookmarks: Vec<DirectoryBookmarkRecord>,
278 directory_history: Vec<DirectoryHistoryRecord>,
279 pending_requests: Vec<PendingServerRequestRecord>,
280 },
281 Response {
282 request_id: String,
283 success: bool,
284 #[serde(skip_serializing_if = "Option::is_none")]
285 data: Option<Value>,
286 #[serde(skip_serializing_if = "Option::is_none")]
287 error: Option<ApiError>,
288 },
289 Event {
290 seq: i64,
291 event_type: String,
292 #[serde(skip_serializing_if = "Option::is_none")]
293 runtime_id: Option<String>,
294 #[serde(skip_serializing_if = "Option::is_none")]
295 thread_id: Option<String>,
296 payload: Value,
297 },
298 Pong {
299 server_time_ms: i64,
300 },
301}
302
303#[derive(Debug, Deserialize)]
304#[serde(rename_all = "camelCase")]
305pub struct GetRuntimeStatusRequest {
306 pub runtime_id: Option<String>,
307}
308
309#[derive(Debug, Deserialize)]
310#[serde(rename_all = "camelCase")]
311pub struct StartRuntimeRequest {
312 pub runtime_id: Option<String>,
313 pub display_name: Option<String>,
314 pub codex_home: Option<String>,
315 pub codex_binary: Option<String>,
316 pub auto_start: Option<bool>,
317}
318
319#[derive(Debug, Deserialize)]
320#[serde(rename_all = "camelCase")]
321pub struct StopRuntimeRequest {
322 pub runtime_id: String,
323}
324
325#[derive(Debug, Deserialize)]
326#[serde(rename_all = "camelCase")]
327pub struct RestartRuntimeRequest {
328 pub runtime_id: String,
329}
330
331#[derive(Debug, Deserialize)]
332#[serde(rename_all = "camelCase")]
333pub struct PruneRuntimeRequest {
334 pub runtime_id: String,
335}
336
337#[derive(Debug, Deserialize)]
338#[serde(rename_all = "camelCase")]
339pub struct CreateDirectoryBookmarkRequest {
340 pub path: String,
341 pub display_name: Option<String>,
342}
343
344#[derive(Debug, Deserialize)]
345#[serde(rename_all = "camelCase")]
346pub struct RemoveDirectoryBookmarkRequest {
347 pub path: String,
348}
349
350#[derive(Debug, Deserialize)]
351#[serde(rename_all = "camelCase")]
352pub struct ReadDirectoryRequest {
353 pub runtime_id: Option<String>,
354 pub path: String,
355}
356
357#[derive(Debug, Deserialize)]
358#[serde(rename_all = "camelCase")]
359pub struct ListThreadsRequest {
360 pub directory_prefix: Option<String>,
361 pub runtime_id: Option<String>,
362 pub limit: Option<usize>,
363 pub cursor: Option<String>,
364 pub archived: Option<bool>,
365 pub search_term: Option<String>,
366}
367
368#[derive(Debug, Deserialize)]
369#[serde(rename_all = "camelCase")]
370pub struct StartThreadRequest {
371 pub runtime_id: Option<String>,
372 pub cwd: String,
373 pub model: Option<String>,
374 pub name: Option<String>,
375 pub note: Option<String>,
376}
377
378#[derive(Debug, Deserialize)]
379#[serde(rename_all = "camelCase")]
380pub struct ResumeThreadRequest {
381 pub thread_id: String,
382}
383
384#[derive(Debug, Deserialize)]
385#[serde(rename_all = "camelCase")]
386pub struct ReadThreadRequest {
387 pub thread_id: String,
388}
389
390#[derive(Debug, Deserialize)]
391#[serde(rename_all = "camelCase")]
392pub struct StageInputImageRequest {
393 pub file_name: Option<String>,
394 pub mime_type: Option<String>,
395 pub base64_data: String,
396}
397
398#[derive(Debug, Clone, Serialize, Deserialize)]
399#[serde(rename_all = "camelCase")]
400pub struct StagedInputImage {
401 pub local_path: String,
402 pub display_name: Option<String>,
403 pub mime_type: Option<String>,
404 pub size_bytes: i64,
405}
406
407#[derive(Debug, Clone, Serialize, Deserialize)]
408#[serde(tag = "type", rename_all = "camelCase")]
409pub enum SendTurnInputItem {
410 Text { text: String },
411 LocalImage { path: String },
412}
413
414#[derive(Debug, Deserialize)]
415#[serde(rename_all = "camelCase")]
416pub struct SendTurnRequest {
417 pub thread_id: String,
418 #[serde(default)]
419 pub text: String,
420 pub input_items: Option<Vec<SendTurnInputItem>>,
421}
422
423#[derive(Debug, Deserialize)]
424#[serde(rename_all = "camelCase")]
425pub struct InterruptTurnRequest {
426 pub thread_id: String,
427 pub turn_id: String,
428}
429
430#[derive(Debug, Deserialize)]
431#[serde(rename_all = "camelCase")]
432pub struct UpdateThreadRequest {
433 pub thread_id: String,
434 pub name: Option<String>,
435 pub note: Option<String>,
436}
437
438#[derive(Debug, Deserialize)]
439#[serde(rename_all = "camelCase")]
440pub struct ArchiveThreadRequest {
441 pub thread_id: String,
442}
443
444#[derive(Debug, Deserialize)]
445#[serde(rename_all = "camelCase")]
446pub struct UnarchiveThreadRequest {
447 pub thread_id: String,
448}
449
450#[derive(Debug, Deserialize)]
451#[serde(rename_all = "camelCase")]
452pub struct RespondPendingRequestRequest {
453 pub request_id: String,
454 pub response: Value,
455}
456
457pub fn ok_response(request_id: String, data: Value) -> ServerEnvelope {
458 ServerEnvelope::Response {
459 request_id,
460 success: true,
461 data: Some(data),
462 error: None,
463 }
464}
465
466pub fn error_response(request_id: String, error: ApiError) -> ServerEnvelope {
467 ServerEnvelope::Response {
468 request_id,
469 success: false,
470 data: None,
471 error: Some(error),
472 }
473}
474
475pub fn event_envelope(event: PersistedEvent) -> ServerEnvelope {
476 ServerEnvelope::Event {
477 seq: event.seq,
478 event_type: event.event_type,
479 runtime_id: event.runtime_id,
480 thread_id: event.thread_id,
481 payload: event.payload,
482 }
483}
484
485pub fn now_millis() -> i64 {
486 let now = std::time::SystemTime::now();
487 let since_epoch = now
488 .duration_since(std::time::UNIX_EPOCH)
489 .unwrap_or_default();
490 since_epoch.as_millis() as i64
491}
492
493pub fn json_string(value: &Value) -> String {
494 match value {
495 Value::String(inner) => inner.clone(),
496 _ => value.to_string(),
497 }
498}
499
500pub fn require_payload<T: for<'de> Deserialize<'de>>(payload: Value) -> Result<T> {
501 Ok(serde_json::from_value(payload)?)
502}
503
504pub fn status_payload(runtime: &RuntimeSummary) -> Value {
505 json!({ "runtime": runtime })
506}
507
508pub fn runtime_list_payload(runtimes: &[RuntimeSummary]) -> Value {
509 json!({ "runtimes": runtimes })
510}