Skip to main content

opencode_sdk/types/
event.rs

1//! SSE event types for opencode_rs.
2//!
3//! Contains 40 event variants matching OpenCode's server.ts.
4
5use crate::types::error::APIError;
6use crate::types::permission::{PermissionReply, PermissionRequest};
7use crate::types::session::Session;
8use serde::{Deserialize, Serialize};
9
10/// Wrapper for events from /global/event which include directory context.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct GlobalEventEnvelope {
13    /// Directory context for the event.
14    pub directory: String,
15    /// The actual event payload.
16    pub payload: Event,
17}
18
19/// SSE Event from OpenCode server (40 variants).
20#[derive(Debug, Clone, Serialize, Deserialize)]
21#[serde(tag = "type")]
22pub enum Event {
23    // ==================== Server/Instance (4) ====================
24    /// Server connection established.
25    #[serde(rename = "server.connected")]
26    ServerConnected {
27        /// Event properties.
28        #[serde(default)]
29        properties: serde_json::Value,
30    },
31
32    /// Server heartbeat (sent periodically).
33    #[serde(rename = "server.heartbeat")]
34    ServerHeartbeat {
35        /// Event properties.
36        #[serde(default)]
37        properties: serde_json::Value,
38    },
39
40    /// Server instance disposed.
41    #[serde(rename = "server.instance.disposed")]
42    ServerInstanceDisposed {
43        /// Event properties.
44        #[serde(default)]
45        properties: serde_json::Value,
46    },
47
48    /// Global disposed.
49    #[serde(rename = "global.disposed")]
50    GlobalDisposed {
51        /// Event properties.
52        #[serde(default)]
53        properties: serde_json::Value,
54    },
55
56    // ==================== Session (8) ====================
57    /// Session created.
58    #[serde(rename = "session.created")]
59    SessionCreated {
60        /// Event properties with full session info.
61        properties: SessionInfoProps,
62    },
63
64    /// Session updated.
65    #[serde(rename = "session.updated")]
66    SessionUpdated {
67        /// Event properties with full session info.
68        properties: SessionInfoProps,
69    },
70
71    /// Session deleted.
72    #[serde(rename = "session.deleted")]
73    SessionDeleted {
74        /// Event properties with full session info.
75        properties: SessionInfoProps,
76    },
77
78    /// Session diff.
79    #[serde(rename = "session.diff")]
80    SessionDiff {
81        /// Event properties.
82        #[serde(default)]
83        properties: serde_json::Value,
84    },
85
86    /// Session error.
87    #[serde(rename = "session.error")]
88    SessionError {
89        /// Event properties with typed error.
90        properties: SessionErrorProps,
91    },
92
93    /// Session compacted.
94    #[serde(rename = "session.compacted")]
95    SessionCompacted {
96        /// Event properties.
97        #[serde(default)]
98        properties: serde_json::Value,
99    },
100
101    /// Session status changed.
102    #[serde(rename = "session.status")]
103    SessionStatus {
104        /// Event properties.
105        #[serde(default)]
106        properties: serde_json::Value,
107    },
108
109    /// Session became idle.
110    #[serde(rename = "session.idle")]
111    SessionIdle {
112        /// Event properties with session ID.
113        properties: SessionIdleProps,
114    },
115
116    // ==================== Messages (4) ====================
117    /// Message updated.
118    #[serde(rename = "message.updated")]
119    MessageUpdated {
120        /// Event properties with full message info.
121        properties: MessageUpdatedProps,
122    },
123
124    /// Message removed.
125    #[serde(rename = "message.removed")]
126    MessageRemoved {
127        /// Event properties with session and message IDs.
128        properties: MessageRemovedProps,
129    },
130
131    /// Message part updated (streaming).
132    #[serde(rename = "message.part.updated")]
133    MessagePartUpdated {
134        /// Event properties (boxed to reduce enum size).
135        properties: Box<MessagePartEventProps>,
136    },
137
138    /// Message part removed.
139    #[serde(rename = "message.part.removed")]
140    MessagePartRemoved {
141        /// Event properties.
142        #[serde(default)]
143        properties: serde_json::Value,
144    },
145
146    // ==================== PTY (4) ====================
147    /// PTY created.
148    #[serde(rename = "pty.created")]
149    PtyCreated {
150        /// Event properties.
151        #[serde(default)]
152        properties: serde_json::Value,
153    },
154
155    /// PTY updated.
156    #[serde(rename = "pty.updated")]
157    PtyUpdated {
158        /// Event properties.
159        #[serde(default)]
160        properties: serde_json::Value,
161    },
162
163    /// PTY exited.
164    #[serde(rename = "pty.exited")]
165    PtyExited {
166        /// Event properties.
167        #[serde(default)]
168        properties: serde_json::Value,
169    },
170
171    /// PTY deleted.
172    #[serde(rename = "pty.deleted")]
173    PtyDeleted {
174        /// Event properties.
175        #[serde(default)]
176        properties: serde_json::Value,
177    },
178
179    // ==================== Permissions (4) ====================
180    /// Permission updated.
181    #[serde(rename = "permission.updated")]
182    PermissionUpdated {
183        /// Event properties.
184        #[serde(default)]
185        properties: serde_json::Value,
186    },
187
188    /// Permission replied.
189    #[serde(rename = "permission.replied")]
190    PermissionReplied {
191        /// Event properties with reply info.
192        properties: PermissionRepliedProps,
193    },
194
195    /// Permission asked.
196    #[serde(rename = "permission.asked")]
197    PermissionAsked {
198        /// Event properties with permission request.
199        properties: PermissionAskedProps,
200    },
201
202    /// Permission replied next.
203    #[serde(rename = "permission.replied-next")]
204    PermissionRepliedNext {
205        /// Event properties with reply info.
206        properties: PermissionRepliedProps,
207    },
208
209    // ==================== Project/Files (4) ====================
210    /// Project updated.
211    #[serde(rename = "project.updated")]
212    ProjectUpdated {
213        /// Event properties.
214        #[serde(default)]
215        properties: serde_json::Value,
216    },
217
218    /// File edited.
219    #[serde(rename = "file.edited")]
220    FileEdited {
221        /// Event properties.
222        #[serde(default)]
223        properties: serde_json::Value,
224    },
225
226    /// File watcher updated.
227    #[serde(rename = "file.watcher.updated")]
228    FileWatcherUpdated {
229        /// Event properties.
230        #[serde(default)]
231        properties: serde_json::Value,
232    },
233
234    /// VCS branch updated.
235    #[serde(rename = "vcs.branch.updated")]
236    VcsBranchUpdated {
237        /// Event properties.
238        #[serde(default)]
239        properties: serde_json::Value,
240    },
241
242    // ==================== LSP/Tools (4) ====================
243    /// LSP updated.
244    #[serde(rename = "lsp.updated")]
245    LspUpdated {
246        /// Event properties.
247        #[serde(default)]
248        properties: serde_json::Value,
249    },
250
251    /// LSP client diagnostics.
252    #[serde(rename = "lsp.client.diagnostics")]
253    LspClientDiagnostics {
254        /// Event properties.
255        #[serde(default)]
256        properties: serde_json::Value,
257    },
258
259    /// Command executed.
260    #[serde(rename = "command.executed")]
261    CommandExecuted {
262        /// Event properties.
263        #[serde(default)]
264        properties: serde_json::Value,
265    },
266
267    /// MCP tools changed.
268    #[serde(rename = "mcp.tools.changed")]
269    McpToolsChanged {
270        /// Event properties.
271        #[serde(default)]
272        properties: serde_json::Value,
273    },
274
275    // ==================== Installation (3) ====================
276    /// Installation updated.
277    #[serde(rename = "installation.updated")]
278    InstallationUpdated {
279        /// Event properties.
280        #[serde(default)]
281        properties: serde_json::Value,
282    },
283
284    /// Installation update available.
285    #[serde(rename = "installation.update-available")]
286    InstallationUpdateAvailable {
287        /// Event properties.
288        #[serde(default)]
289        properties: serde_json::Value,
290    },
291
292    /// IDE installed.
293    #[serde(rename = "ide.installed")]
294    IdeInstalled {
295        /// Event properties.
296        #[serde(default)]
297        properties: serde_json::Value,
298    },
299
300    // ==================== TUI (4) ====================
301    /// TUI prompt append.
302    #[serde(rename = "tui.prompt.append")]
303    TuiPromptAppend {
304        /// Event properties.
305        #[serde(default)]
306        properties: serde_json::Value,
307    },
308
309    /// TUI command execute.
310    #[serde(rename = "tui.command.execute")]
311    TuiCommandExecute {
312        /// Event properties.
313        #[serde(default)]
314        properties: serde_json::Value,
315    },
316
317    /// TUI toast show.
318    #[serde(rename = "tui.toast.show")]
319    TuiToastShow {
320        /// Event properties.
321        #[serde(default)]
322        properties: serde_json::Value,
323    },
324
325    /// TUI session select.
326    #[serde(rename = "tui.session.select")]
327    TuiSessionSelect {
328        /// Event properties.
329        #[serde(default)]
330        properties: serde_json::Value,
331    },
332
333    // ==================== Todo (1) ====================
334    /// Todo updated.
335    #[serde(rename = "todo.updated")]
336    TodoUpdated {
337        /// Event properties.
338        #[serde(default)]
339        properties: serde_json::Value,
340    },
341
342    /// Fallback for unknown event types.
343    #[serde(other)]
344    Unknown,
345}
346
347// ==================== Session Event Properties ====================
348
349/// Properties for session events (created/updated/deleted).
350#[derive(Debug, Clone, Serialize, Deserialize)]
351#[serde(rename_all = "camelCase")]
352pub struct SessionInfoProps {
353    /// Full session info.
354    pub info: Session,
355    /// Additional properties.
356    #[serde(flatten)]
357    pub extra: serde_json::Value,
358}
359
360/// Properties for session.idle events.
361#[derive(Debug, Clone, Serialize, Deserialize)]
362#[serde(rename_all = "camelCase")]
363pub struct SessionIdleProps {
364    /// Session ID.
365    #[serde(default, alias = "sessionID")]
366    pub session_id: Option<String>,
367    /// Additional properties.
368    #[serde(flatten)]
369    pub extra: serde_json::Value,
370}
371
372/// Error union that can be APIError or unknown value.
373#[derive(Debug, Clone, Serialize, Deserialize)]
374#[serde(untagged)]
375pub enum AssistantError {
376    /// Known API error.
377    Api(APIError),
378    /// Unknown error format (forward compatibility).
379    Unknown(serde_json::Value),
380}
381
382/// Properties for session error events.
383#[derive(Debug, Clone, Serialize, Deserialize)]
384#[serde(rename_all = "camelCase")]
385pub struct SessionErrorProps {
386    /// Session ID.
387    #[serde(default, alias = "sessionID")]
388    pub session_id: Option<String>,
389    /// Typed error.
390    #[serde(default)]
391    pub error: Option<AssistantError>,
392    /// Additional properties.
393    #[serde(flatten)]
394    pub extra: serde_json::Value,
395}
396
397// ==================== Message Event Properties ====================
398
399/// Properties for message.updated events.
400#[derive(Debug, Clone, Serialize, Deserialize)]
401#[serde(rename_all = "camelCase")]
402pub struct MessageUpdatedProps {
403    /// Full message info.
404    pub info: crate::types::message::MessageInfo,
405    /// Additional properties.
406    #[serde(flatten)]
407    pub extra: serde_json::Value,
408}
409
410/// Properties for message.removed events.
411#[derive(Debug, Clone, Serialize, Deserialize)]
412#[serde(rename_all = "camelCase")]
413pub struct MessageRemovedProps {
414    /// Session ID.
415    #[serde(alias = "sessionID")]
416    pub session_id: String,
417    /// Message ID.
418    pub message_id: String,
419    /// Additional properties.
420    #[serde(flatten)]
421    pub extra: serde_json::Value,
422}
423
424/// Properties for message part update events.
425#[derive(Debug, Clone, Serialize, Deserialize)]
426#[serde(rename_all = "camelCase")]
427pub struct MessagePartEventProps {
428    /// Session ID.
429    #[serde(default, alias = "sessionID")]
430    pub session_id: Option<String>,
431    /// Message ID.
432    #[serde(default)]
433    pub message_id: Option<String>,
434    /// Part index.
435    #[serde(default)]
436    pub index: Option<usize>,
437    /// Updated part content.
438    #[serde(default)]
439    pub part: Option<crate::types::message::Part>,
440    /// Streaming delta (incremental text).
441    #[serde(default)]
442    pub delta: Option<String>,
443    /// Additional properties.
444    #[serde(flatten)]
445    pub extra: serde_json::Value,
446}
447
448// ==================== Permission Event Properties ====================
449
450/// Properties for permission.asked events.
451#[derive(Debug, Clone, Serialize, Deserialize)]
452#[serde(rename_all = "camelCase")]
453pub struct PermissionAskedProps {
454    /// The permission request (flattened).
455    #[serde(flatten)]
456    pub request: PermissionRequest,
457}
458
459/// Properties for permission.replied events.
460#[derive(Debug, Clone, Serialize, Deserialize)]
461#[serde(rename_all = "camelCase")]
462pub struct PermissionRepliedProps {
463    /// Session ID.
464    #[serde(alias = "sessionID")]
465    pub session_id: String,
466    /// Request ID that was replied to.
467    pub request_id: String,
468    /// The reply given.
469    pub reply: PermissionReply,
470    /// Additional properties.
471    #[serde(flatten)]
472    pub extra: serde_json::Value,
473}
474
475impl Event {
476    /// Extract session_id if present in this event.
477    pub fn session_id(&self) -> Option<&str> {
478        match self {
479            Event::SessionCreated { properties } => Some(&properties.info.id),
480            Event::SessionUpdated { properties } => Some(&properties.info.id),
481            Event::SessionDeleted { properties } => Some(&properties.info.id),
482            Event::SessionIdle { properties } => properties.session_id.as_deref(),
483            Event::SessionError { properties } => properties.session_id.as_deref(),
484            Event::MessageUpdated { properties } => properties.info.session_id.as_deref(),
485            Event::MessageRemoved { properties } => Some(&properties.session_id),
486            Event::MessagePartUpdated { properties } => properties.session_id.as_deref(),
487            Event::PermissionAsked { properties } => properties.request.session_id.as_deref(),
488            Event::PermissionReplied { properties } => Some(&properties.session_id),
489            Event::PermissionRepliedNext { properties } => Some(&properties.session_id),
490            _ => None,
491        }
492    }
493
494    /// Check if this is a heartbeat event.
495    pub fn is_heartbeat(&self) -> bool {
496        matches!(self, Event::ServerHeartbeat { .. })
497    }
498
499    /// Check if this is a connection event.
500    pub fn is_connected(&self) -> bool {
501        matches!(self, Event::ServerConnected { .. })
502    }
503}
504
505#[cfg(test)]
506mod tests {
507    use super::*;
508
509    #[test]
510    fn test_event_deserialize_session_created() {
511        let json = r#"{
512            "type": "session.created",
513            "properties": {
514                "info": {
515                    "id": "sess-123",
516                    "title": "Test Session",
517                    "version": "1.0"
518                }
519            }
520        }"#;
521        let event: Event = serde_json::from_str(json).unwrap();
522        assert!(matches!(event, Event::SessionCreated { .. }));
523        assert_eq!(event.session_id(), Some("sess-123"));
524    }
525
526    #[test]
527    fn test_event_deserialize_heartbeat() {
528        let json = r#"{"type":"server.heartbeat","properties":{}}"#;
529        let event: Event = serde_json::from_str(json).unwrap();
530        assert!(matches!(event, Event::ServerHeartbeat { .. }));
531        assert!(event.is_heartbeat());
532    }
533
534    #[test]
535    fn test_event_deserialize_unknown() {
536        let json = r#"{"type":"some.future.event","properties":{}}"#;
537        let event: Event = serde_json::from_str(json).unwrap();
538        assert!(matches!(event, Event::Unknown));
539    }
540
541    #[test]
542    fn test_message_part_with_delta() {
543        let json = r#"{"type":"message.part.updated","properties":{"sessionId":"s1","messageId":"m1","delta":"Hello"}}"#;
544        let event: Event = serde_json::from_str(json).unwrap();
545        if let Event::MessagePartUpdated { properties } = &event {
546            assert_eq!(properties.delta, Some("Hello".to_string()));
547        } else {
548            panic!("Expected MessagePartUpdated");
549        }
550    }
551
552    #[test]
553    fn test_event_deserialize_pty_created() {
554        let json = r#"{"type":"pty.created","properties":{"id":"pty1"}}"#;
555        let event: Event = serde_json::from_str(json).unwrap();
556        assert!(matches!(event, Event::PtyCreated { .. }));
557    }
558
559    #[test]
560    fn test_event_deserialize_permission_asked() {
561        let json = r#"{
562            "type": "permission.asked",
563            "properties": {
564                "id": "req-123",
565                "sessionId": "sess-456",
566                "permission": "file.write",
567                "patterns": ["**/*.rs"]
568            }
569        }"#;
570        let event: Event = serde_json::from_str(json).unwrap();
571        assert!(matches!(event, Event::PermissionAsked { .. }));
572        assert_eq!(event.session_id(), Some("sess-456"));
573    }
574
575    #[test]
576    fn test_event_deserialize_permission_replied() {
577        let json = r#"{
578            "type": "permission.replied",
579            "properties": {
580                "sessionId": "sess-456",
581                "requestId": "req-123",
582                "reply": "always"
583            }
584        }"#;
585        let event: Event = serde_json::from_str(json).unwrap();
586        assert!(matches!(event, Event::PermissionReplied { .. }));
587        assert_eq!(event.session_id(), Some("sess-456"));
588    }
589
590    #[test]
591    fn test_event_deserialize_message_updated() {
592        let json = r#"{
593            "type": "message.updated",
594            "properties": {
595                "info": {
596                    "id": "msg-123",
597                    "sessionId": "sess-456",
598                    "role": "assistant",
599                    "time": {"created": 1234567890}
600                }
601            }
602        }"#;
603        let event: Event = serde_json::from_str(json).unwrap();
604        assert!(matches!(event, Event::MessageUpdated { .. }));
605        assert_eq!(event.session_id(), Some("sess-456"));
606    }
607
608    #[test]
609    fn test_event_deserialize_message_removed() {
610        let json = r#"{
611            "type": "message.removed",
612            "properties": {
613                "sessionId": "sess-456",
614                "messageId": "msg-123"
615            }
616        }"#;
617        let event: Event = serde_json::from_str(json).unwrap();
618        assert!(matches!(event, Event::MessageRemoved { .. }));
619        assert_eq!(event.session_id(), Some("sess-456"));
620    }
621
622    #[test]
623    fn test_event_deserialize_session_error() {
624        let json = r#"{
625            "type": "session.error",
626            "properties": {
627                "sessionId": "sess-456",
628                "error": {"message": "Something went wrong", "isRetryable": false}
629            }
630        }"#;
631        let event: Event = serde_json::from_str(json).unwrap();
632        if let Event::SessionError { properties } = &event {
633            assert!(properties.error.is_some());
634            if let Some(AssistantError::Api(err)) = &properties.error {
635                assert_eq!(err.message, "Something went wrong");
636            } else {
637                panic!("Expected APIError");
638            }
639        } else {
640            panic!("Expected SessionError");
641        }
642    }
643
644    #[test]
645    fn test_event_deserialize_session_idle_with_session_id_alias() {
646        let json = r#"{
647            "type": "session.idle",
648            "properties": {
649                "sessionID": "sess-456"
650            }
651        }"#;
652        let event: Event = serde_json::from_str(json).unwrap();
653        assert!(matches!(event, Event::SessionIdle { .. }));
654        assert_eq!(event.session_id(), Some("sess-456"));
655    }
656
657    #[test]
658    fn test_event_deserialize_message_removed_with_session_id_alias() {
659        let json = r#"{
660            "type": "message.removed",
661            "properties": {
662                "sessionID": "sess-456",
663                "messageId": "msg-123"
664            }
665        }"#;
666        let event: Event = serde_json::from_str(json).unwrap();
667        assert!(matches!(event, Event::MessageRemoved { .. }));
668        assert_eq!(event.session_id(), Some("sess-456"));
669    }
670
671    #[test]
672    fn test_event_deserialize_todo_updated() {
673        let json = r#"{"type":"todo.updated","properties":{}}"#;
674        let event: Event = serde_json::from_str(json).unwrap();
675        assert!(matches!(event, Event::TodoUpdated { .. }));
676    }
677
678    // TODO(3): Add tests for GlobalEventEnvelope deserialization and round-trip serialization
679}