1use std::collections::HashMap;
8
9use serde::{Deserialize, Serialize};
10
11use super::{
12 session::{Message, Part, Session},
13 shared::SessionError,
14};
15use crate::client::Opencode;
16
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
25#[serde(tag = "type")]
26pub enum EventListResponse {
27 #[serde(rename = "installation.updated")]
29 InstallationUpdated {
30 properties: InstallationUpdatedProps,
32 },
33
34 #[serde(rename = "lsp.client.diagnostics")]
36 LspClientDiagnostics {
37 properties: LspClientDiagnosticsProps,
39 },
40
41 #[serde(rename = "message.updated")]
43 MessageUpdated {
44 properties: MessageUpdatedProps,
46 },
47
48 #[serde(rename = "message.removed")]
50 MessageRemoved {
51 properties: MessageRemovedProps,
53 },
54
55 #[serde(rename = "message.part.updated")]
57 MessagePartUpdated {
58 properties: MessagePartUpdatedProps,
60 },
61
62 #[serde(rename = "message.part.removed")]
64 MessagePartRemoved {
65 properties: MessagePartRemovedProps,
67 },
68
69 #[serde(rename = "storage.write")]
71 StorageWrite {
72 properties: StorageWriteProps,
74 },
75
76 #[serde(rename = "permission.updated")]
78 PermissionUpdated {
79 properties: PermissionUpdatedProps,
81 },
82
83 #[serde(rename = "file.edited")]
85 FileEdited {
86 properties: FileEditedProps,
88 },
89
90 #[serde(rename = "session.updated")]
92 SessionUpdated {
93 properties: SessionUpdatedProps,
95 },
96
97 #[serde(rename = "session.deleted")]
99 SessionDeleted {
100 properties: SessionDeletedProps,
102 },
103
104 #[serde(rename = "session.idle")]
106 SessionIdle {
107 properties: SessionIdleProps,
109 },
110
111 #[serde(rename = "session.error")]
113 SessionError {
114 properties: SessionErrorProps,
116 },
117
118 #[serde(rename = "file.watcher.updated")]
120 FileWatcherUpdated {
121 properties: FileWatcherUpdatedProps,
123 },
124
125 #[serde(rename = "ide.installed")]
127 IdeInstalled {
128 properties: IdeInstalledProps,
130 },
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
139pub struct InstallationUpdatedProps {
140 pub version: String,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
146pub struct LspClientDiagnosticsProps {
147 pub path: String,
149 #[serde(rename = "serverID")]
151 pub server_id: String,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
156pub struct MessageUpdatedProps {
157 pub info: Message,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
163pub struct MessageRemovedProps {
164 #[serde(rename = "messageID")]
166 pub message_id: String,
167 #[serde(rename = "sessionID")]
169 pub session_id: String,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
174pub struct MessagePartUpdatedProps {
175 pub part: Part,
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
181pub struct MessagePartRemovedProps {
182 #[serde(rename = "messageID")]
184 pub message_id: String,
185 #[serde(rename = "partID")]
187 pub part_id: String,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
192pub struct StorageWriteProps {
193 pub key: String,
195 #[serde(skip_serializing_if = "Option::is_none")]
197 pub content: Option<serde_json::Value>,
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
202pub struct PermissionUpdatedProps {
203 pub id: String,
205 pub metadata: HashMap<String, serde_json::Value>,
207 #[serde(rename = "sessionID")]
209 pub session_id: String,
210 pub time: PermissionTime,
212 pub title: String,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
218pub struct PermissionTime {
219 pub created: f64,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
225pub struct FileEditedProps {
226 pub file: String,
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
232pub struct SessionUpdatedProps {
233 pub info: Session,
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
239pub struct SessionDeletedProps {
240 pub info: Session,
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
246pub struct SessionIdleProps {
247 #[serde(rename = "sessionID")]
249 pub session_id: String,
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
254pub struct SessionErrorProps {
255 #[serde(skip_serializing_if = "Option::is_none")]
257 pub error: Option<SessionError>,
258 #[serde(rename = "sessionID")]
260 #[serde(skip_serializing_if = "Option::is_none")]
261 pub session_id: Option<String>,
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
266pub enum FileWatcherEvent {
267 #[serde(rename = "rename")]
269 Rename,
270 #[serde(rename = "change")]
272 Change,
273}
274
275#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
277pub struct FileWatcherUpdatedProps {
278 pub event: FileWatcherEvent,
280 pub file: String,
282}
283
284#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
286pub struct IdeInstalledProps {
287 pub ide: String,
289}
290
291pub struct EventResource<'a> {
297 client: &'a Opencode,
298}
299
300impl<'a> EventResource<'a> {
301 pub(crate) const fn new(client: &'a Opencode) -> Self {
303 Self { client }
304 }
305
306 pub async fn list(
311 &self,
312 ) -> Result<crate::streaming::SseStream<EventListResponse>, crate::error::OpencodeError> {
313 self.client.get_stream("/event").await
314 }
315}
316
317#[cfg(test)]
322mod tests {
323 use serde_json::json;
324
325 use super::*;
326 use crate::resources::session::{UserMessage, UserMessageTime};
327
328 #[test]
331 fn installation_updated_round_trip() {
332 let event = EventListResponse::InstallationUpdated {
333 properties: InstallationUpdatedProps { version: "1.2.3".into() },
334 };
335 let json_str = serde_json::to_string(&event).unwrap();
336 assert!(json_str.contains(r#""type":"installation.updated"#));
337 assert!(json_str.contains(r#""version":"1.2.3"#));
338 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
339 assert_eq!(event, back);
340 }
341
342 #[test]
345 fn message_updated_round_trip() {
346 let msg = Message::User(UserMessage {
347 id: "msg_u001".into(),
348 session_id: "sess_001".into(),
349 time: UserMessageTime { created: 1_700_000_100.0 },
350 });
351
352 let event = EventListResponse::MessageUpdated {
353 properties: MessageUpdatedProps { info: msg.clone() },
354 };
355 let json_str = serde_json::to_string(&event).unwrap();
356 assert!(json_str.contains(r#""type":"message.updated"#));
357 assert!(json_str.contains(r#""role":"user"#));
358 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
359 assert_eq!(event, back);
360 }
361
362 #[test]
365 fn session_error_round_trip() {
366 use crate::resources::shared::{SessionError as SE, UnknownErrorData};
367
368 let event = EventListResponse::SessionError {
369 properties: SessionErrorProps {
370 error: Some(SE::UnknownError {
371 data: UnknownErrorData { message: "something broke".into() },
372 }),
373 session_id: Some("sess_err_001".into()),
374 },
375 };
376 let json_str = serde_json::to_string(&event).unwrap();
377 assert!(json_str.contains(r#""type":"session.error"#));
378 assert!(json_str.contains(r#""name":"UnknownError"#));
379 assert!(json_str.contains("something broke"));
380 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
381 assert_eq!(event, back);
382 }
383
384 #[test]
385 fn session_error_empty_round_trip() {
386 let event = EventListResponse::SessionError {
387 properties: SessionErrorProps { error: None, session_id: None },
388 };
389 let json_str = serde_json::to_string(&event).unwrap();
390 assert!(!json_str.contains(r#""error""#));
392 assert!(!json_str.contains(r#""sessionID""#));
393 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
394 assert_eq!(event, back);
395 }
396
397 #[test]
400 fn file_watcher_updated_round_trip() {
401 let event = EventListResponse::FileWatcherUpdated {
402 properties: FileWatcherUpdatedProps {
403 event: FileWatcherEvent::Rename,
404 file: "src/main.rs".into(),
405 },
406 };
407 let json_str = serde_json::to_string(&event).unwrap();
408 assert!(json_str.contains(r#""type":"file.watcher.updated"#));
409 assert!(json_str.contains(r#""event":"rename"#));
410 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
411 assert_eq!(event, back);
412
413 let event2 = EventListResponse::FileWatcherUpdated {
415 properties: FileWatcherUpdatedProps {
416 event: FileWatcherEvent::Change,
417 file: "Cargo.toml".into(),
418 },
419 };
420 let json_str2 = serde_json::to_string(&event2).unwrap();
421 assert!(json_str2.contains(r#""event":"change"#));
422 let back2: EventListResponse = serde_json::from_str(&json_str2).unwrap();
423 assert_eq!(event2, back2);
424 }
425
426 #[test]
429 fn storage_write_round_trip() {
430 let event = EventListResponse::StorageWrite {
431 properties: StorageWriteProps {
432 key: "my-key".into(),
433 content: Some(json!({"nested": true, "count": 42})),
434 },
435 };
436 let json_str = serde_json::to_string(&event).unwrap();
437 assert!(json_str.contains(r#""type":"storage.write"#));
438 assert!(json_str.contains(r#""key":"my-key"#));
439 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
440 assert_eq!(event, back);
441 }
442
443 #[test]
444 fn storage_write_no_content_round_trip() {
445 let event = EventListResponse::StorageWrite {
446 properties: StorageWriteProps { key: "empty-key".into(), content: None },
447 };
448 let json_str = serde_json::to_string(&event).unwrap();
449 assert!(!json_str.contains("content"));
450 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
451 assert_eq!(event, back);
452 }
453
454 #[test]
457 fn deserialize_from_raw_json() {
458 let raw = r#"{
459 "type": "ide.installed",
460 "properties": { "ide": "vscode" }
461 }"#;
462 let event: EventListResponse = serde_json::from_str(raw).unwrap();
463 assert_eq!(
464 event,
465 EventListResponse::IdeInstalled {
466 properties: IdeInstalledProps { ide: "vscode".into() }
467 }
468 );
469 }
470
471 #[test]
472 fn deserialize_permission_updated() {
473 let raw = r#"{
474 "type": "permission.updated",
475 "properties": {
476 "id": "perm_001",
477 "metadata": {"tool": "bash"},
478 "sessionID": "sess_001",
479 "time": {"created": 1700000000.0},
480 "title": "Run bash command"
481 }
482 }"#;
483 let event: EventListResponse = serde_json::from_str(raw).unwrap();
484 match &event {
485 EventListResponse::PermissionUpdated { properties } => {
486 assert_eq!(properties.id, "perm_001");
487 assert_eq!(properties.session_id, "sess_001");
488 assert_eq!(properties.title, "Run bash command");
489 assert_eq!(properties.time.created, 1_700_000_000.0);
490 assert_eq!(properties.metadata.get("tool"), Some(&json!("bash")));
491 }
492 other => panic!("expected PermissionUpdated, got {other:?}"),
493 }
494 let json_str = serde_json::to_string(&event).unwrap();
496 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
497 assert_eq!(event, back);
498 }
499
500 #[test]
503 fn lsp_client_diagnostics_round_trip() {
504 let event = EventListResponse::LspClientDiagnostics {
505 properties: LspClientDiagnosticsProps {
506 path: "src/main.rs".into(),
507 server_id: "rust-analyzer".into(),
508 },
509 };
510 let json_str = serde_json::to_string(&event).unwrap();
511 assert!(json_str.contains(r#""type":"lsp.client.diagnostics"#));
512 assert!(json_str.contains(r#""serverID":"rust-analyzer"#));
513 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
514 assert_eq!(event, back);
515 }
516
517 #[test]
518 fn message_removed_round_trip() {
519 let event = EventListResponse::MessageRemoved {
520 properties: MessageRemovedProps {
521 message_id: "msg_del_001".into(),
522 session_id: "sess_001".into(),
523 },
524 };
525 let json_str = serde_json::to_string(&event).unwrap();
526 assert!(json_str.contains(r#""type":"message.removed"#));
527 assert!(json_str.contains("messageID"));
528 assert!(json_str.contains("sessionID"));
529 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
530 assert_eq!(event, back);
531 }
532
533 #[test]
534 fn message_part_updated_round_trip() {
535 use crate::resources::session::{Part, TextPart};
536
537 let event = EventListResponse::MessagePartUpdated {
538 properties: MessagePartUpdatedProps {
539 part: Part::Text(TextPart {
540 id: "p_upd_001".into(),
541 message_id: "msg_001".into(),
542 session_id: "sess_001".into(),
543 text: "updated text".into(),
544 synthetic: None,
545 time: None,
546 }),
547 },
548 };
549 let json_str = serde_json::to_string(&event).unwrap();
550 assert!(json_str.contains(r#""type":"message.part.updated"#));
551 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
552 assert_eq!(event, back);
553 }
554
555 #[test]
556 fn message_part_removed_round_trip() {
557 let event = EventListResponse::MessagePartRemoved {
558 properties: MessagePartRemovedProps {
559 message_id: "msg_001".into(),
560 part_id: "p_del_001".into(),
561 },
562 };
563 let json_str = serde_json::to_string(&event).unwrap();
564 assert!(json_str.contains(r#""type":"message.part.removed"#));
565 assert!(json_str.contains("messageID"));
566 assert!(json_str.contains("partID"));
567 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
568 assert_eq!(event, back);
569 }
570
571 #[test]
572 fn file_edited_round_trip() {
573 let event = EventListResponse::FileEdited {
574 properties: FileEditedProps { file: "src/lib.rs".into() },
575 };
576 let json_str = serde_json::to_string(&event).unwrap();
577 assert!(json_str.contains(r#""type":"file.edited"#));
578 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
579 assert_eq!(event, back);
580 }
581
582 #[test]
583 fn session_updated_round_trip() {
584 let event = EventListResponse::SessionUpdated {
585 properties: SessionUpdatedProps {
586 info: Session {
587 id: "sess_upd".into(),
588 time: crate::resources::session::SessionTime {
589 created: 1_700_000_000.0,
590 updated: 1_700_001_000.0,
591 },
592 title: "Updated".into(),
593 version: "1".into(),
594 parent_id: None,
595 revert: None,
596 share: None,
597 },
598 },
599 };
600 let json_str = serde_json::to_string(&event).unwrap();
601 assert!(json_str.contains(r#""type":"session.updated"#));
602 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
603 assert_eq!(event, back);
604 }
605
606 #[test]
607 fn session_deleted_round_trip() {
608 let event = EventListResponse::SessionDeleted {
609 properties: SessionDeletedProps {
610 info: Session {
611 id: "sess_del".into(),
612 time: crate::resources::session::SessionTime {
613 created: 1_700_000_000.0,
614 updated: 1_700_000_000.0,
615 },
616 title: "Deleted".into(),
617 version: "1".into(),
618 parent_id: None,
619 revert: None,
620 share: None,
621 },
622 },
623 };
624 let json_str = serde_json::to_string(&event).unwrap();
625 assert!(json_str.contains(r#""type":"session.deleted"#));
626 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
627 assert_eq!(event, back);
628 }
629
630 #[test]
631 fn session_idle_round_trip() {
632 let event = EventListResponse::SessionIdle {
633 properties: SessionIdleProps { session_id: "sess_idle_001".into() },
634 };
635 let json_str = serde_json::to_string(&event).unwrap();
636 assert!(json_str.contains(r#""type":"session.idle"#));
637 assert!(json_str.contains("sessionID"));
638 let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
639 assert_eq!(event, back);
640 }
641
642 #[test]
643 fn storage_write_null_content() {
644 let raw = r#"{
646 "type": "storage.write",
647 "properties": { "key": "k", "content": null }
648 }"#;
649 let event: EventListResponse = serde_json::from_str(raw).unwrap();
650 match &event {
651 EventListResponse::StorageWrite { properties } => {
652 assert_eq!(properties.key, "k");
653 assert_eq!(properties.content, None);
654 }
655 other => panic!("expected StorageWrite, got {other:?}"),
656 }
657 }
658
659 #[test]
660 fn session_error_both_fields_null() {
661 let raw = r#"{
663 "type": "session.error",
664 "properties": { "error": null, "sessionID": null }
665 }"#;
666 let event: EventListResponse = serde_json::from_str(raw).unwrap();
667 match &event {
668 EventListResponse::SessionError { properties } => {
669 assert_eq!(properties.error, None);
670 assert_eq!(properties.session_id, None);
671 }
672 other => panic!("expected SessionError, got {other:?}"),
673 }
674 }
675}