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