Skip to main content

opencode_sdk_rs/resources/
event.rs

1//! Event resource types and the `EventResource` struct.
2//!
3//! Events are delivered via Server-Sent Events (SSE).  The [`EventResource`]
4//! will expose a streaming `list()` method once SSE support is wired up
5//! (Task 3.1).  For now only the data types are defined.
6
7use serde::{Deserialize, Serialize};
8
9use super::{
10    session::{FileDiff, Message, Part, Session},
11    shared::SessionError,
12};
13use crate::client::Opencode;
14
15// ---------------------------------------------------------------------------
16// EventListResponse — internally-tagged discriminated union
17// ---------------------------------------------------------------------------
18
19/// A single event from the `/event` SSE stream.
20///
21/// Internally tagged on `"type"` to match the JS SDK representation.
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
23#[serde(tag = "type")]
24pub enum EventListResponse {
25    // ----- installation -----
26    /// An installation was updated to a new version.
27    #[serde(rename = "installation.updated")]
28    InstallationUpdated {
29        /// Payload.
30        properties: InstallationUpdatedProps,
31    },
32
33    /// A newer version is available for installation.
34    #[serde(rename = "installation.update-available")]
35    InstallationUpdateAvailable {
36        /// Payload.
37        properties: InstallationUpdateAvailableProps,
38    },
39
40    // ----- project -----
41    /// A project was updated.
42    #[serde(rename = "project.updated")]
43    ProjectUpdated {
44        /// Payload.
45        properties: ProjectUpdatedProps,
46    },
47
48    // ----- server -----
49    /// A server instance was disposed.
50    #[serde(rename = "server.instance.disposed")]
51    ServerInstanceDisposed {
52        /// Payload.
53        properties: ServerInstanceDisposedProps,
54    },
55
56    /// A server connected.
57    #[serde(rename = "server.connected")]
58    ServerConnected {
59        /// Payload.
60        properties: EmptyProps,
61    },
62
63    // ----- global -----
64    /// Global disposed.
65    #[serde(rename = "global.disposed")]
66    GlobalDisposed {
67        /// Payload.
68        properties: EmptyProps,
69    },
70
71    // ----- lsp -----
72    /// LSP client diagnostics were received.
73    #[serde(rename = "lsp.client.diagnostics")]
74    LspClientDiagnostics {
75        /// Payload.
76        properties: LspClientDiagnosticsProps,
77    },
78
79    /// LSP state was updated.
80    #[serde(rename = "lsp.updated")]
81    LspUpdated {
82        /// Payload.
83        properties: EmptyProps,
84    },
85
86    // ----- file -----
87    /// A file was edited.
88    #[serde(rename = "file.edited")]
89    FileEdited {
90        /// Payload.
91        properties: FileEditedProps,
92    },
93
94    /// A file-watcher event was received.
95    #[serde(rename = "file.watcher.updated")]
96    FileWatcherUpdated {
97        /// Payload.
98        properties: FileWatcherUpdatedProps,
99    },
100
101    // ----- message -----
102    /// A message was updated.
103    #[serde(rename = "message.updated")]
104    MessageUpdated {
105        /// Payload.
106        properties: MessageUpdatedProps,
107    },
108
109    /// A message was removed.
110    #[serde(rename = "message.removed")]
111    MessageRemoved {
112        /// Payload.
113        properties: MessageRemovedProps,
114    },
115
116    /// A message part was updated.
117    #[serde(rename = "message.part.updated")]
118    MessagePartUpdated {
119        /// Payload.
120        properties: MessagePartUpdatedProps,
121    },
122
123    /// A streaming delta for a message part.
124    #[serde(rename = "message.part.delta")]
125    MessagePartDelta {
126        /// Payload.
127        properties: MessagePartDeltaProps,
128    },
129
130    /// A message part was removed.
131    #[serde(rename = "message.part.removed")]
132    MessagePartRemoved {
133        /// Payload.
134        properties: MessagePartRemovedProps,
135    },
136
137    // ----- permission -----
138    /// A permission was asked.
139    #[serde(rename = "permission.asked")]
140    PermissionAsked {
141        /// Payload (complex `PermissionRequest` type, represented as JSON value).
142        properties: serde_json::Value,
143    },
144
145    /// A permission was replied to.
146    #[serde(rename = "permission.replied")]
147    PermissionReplied {
148        /// Payload.
149        properties: PermissionRepliedProps,
150    },
151
152    // ----- session -----
153    /// A session was created.
154    #[serde(rename = "session.created")]
155    SessionCreated {
156        /// Payload.
157        properties: SessionCreatedProps,
158    },
159
160    /// A session was updated.
161    #[serde(rename = "session.updated")]
162    SessionUpdated {
163        /// Payload.
164        properties: SessionUpdatedProps,
165    },
166
167    /// A session was deleted.
168    #[serde(rename = "session.deleted")]
169    SessionDeleted {
170        /// Payload.
171        properties: SessionDeletedProps,
172    },
173
174    /// Session status changed.
175    #[serde(rename = "session.status")]
176    SessionStatus {
177        /// Payload.
178        properties: SessionStatusProps,
179    },
180
181    /// A session became idle.
182    #[serde(rename = "session.idle")]
183    SessionIdle {
184        /// Payload.
185        properties: SessionIdleProps,
186    },
187
188    /// A session diff was produced.
189    #[serde(rename = "session.diff")]
190    SessionDiff {
191        /// Payload.
192        properties: SessionDiffProps,
193    },
194
195    /// A session was compacted.
196    #[serde(rename = "session.compacted")]
197    SessionCompacted {
198        /// Payload.
199        properties: SessionCompactedProps,
200    },
201
202    /// A session encountered an error.
203    #[serde(rename = "session.error")]
204    SessionError {
205        /// Payload.
206        properties: SessionErrorProps,
207    },
208
209    // ----- question -----
210    /// A question was asked.
211    #[serde(rename = "question.asked")]
212    QuestionAsked {
213        /// Payload (complex `QuestionRequest` type, represented as JSON value).
214        properties: serde_json::Value,
215    },
216
217    /// A question was replied to.
218    #[serde(rename = "question.replied")]
219    QuestionReplied {
220        /// Payload.
221        properties: QuestionRepliedProps,
222    },
223
224    /// A question was rejected.
225    #[serde(rename = "question.rejected")]
226    QuestionRejected {
227        /// Payload.
228        properties: QuestionRejectedProps,
229    },
230
231    // ----- todo -----
232    /// Todos were updated.
233    #[serde(rename = "todo.updated")]
234    TodoUpdated {
235        /// Payload.
236        properties: TodoUpdatedProps,
237    },
238
239    // ----- tui -----
240    /// Append text to the TUI prompt.
241    #[serde(rename = "tui.prompt.append")]
242    TuiPromptAppend {
243        /// Payload.
244        properties: TuiPromptAppendProps,
245    },
246
247    /// Execute a TUI command.
248    #[serde(rename = "tui.command.execute")]
249    TuiCommandExecute {
250        /// Payload.
251        properties: TuiCommandExecuteProps,
252    },
253
254    /// Show a TUI toast notification.
255    #[serde(rename = "tui.toast.show")]
256    TuiToastShow {
257        /// Payload.
258        properties: TuiToastShowProps,
259    },
260
261    /// Select a TUI session.
262    #[serde(rename = "tui.session.select")]
263    TuiSessionSelect {
264        /// Payload.
265        properties: TuiSessionSelectProps,
266    },
267
268    // ----- mcp -----
269    /// MCP tools changed.
270    #[serde(rename = "mcp.tools.changed")]
271    McpToolsChanged {
272        /// Payload.
273        properties: McpToolsChangedProps,
274    },
275
276    /// MCP browser open failed.
277    #[serde(rename = "mcp.browser.open.failed")]
278    McpBrowserOpenFailed {
279        /// Payload.
280        properties: McpBrowserOpenFailedProps,
281    },
282
283    // ----- command -----
284    /// A command was executed.
285    #[serde(rename = "command.executed")]
286    CommandExecuted {
287        /// Payload.
288        properties: CommandExecutedProps,
289    },
290
291    // ----- vcs -----
292    /// VCS branch was updated.
293    #[serde(rename = "vcs.branch.updated")]
294    VcsBranchUpdated {
295        /// Payload.
296        properties: VcsBranchUpdatedProps,
297    },
298
299    // ----- pty -----
300    /// A PTY was created.
301    #[serde(rename = "pty.created")]
302    PtyCreated {
303        /// Payload.
304        properties: PtyCreatedProps,
305    },
306
307    /// A PTY was updated.
308    #[serde(rename = "pty.updated")]
309    PtyUpdated {
310        /// Payload.
311        properties: PtyUpdatedProps,
312    },
313
314    /// A PTY exited.
315    #[serde(rename = "pty.exited")]
316    PtyExited {
317        /// Payload.
318        properties: PtyExitedProps,
319    },
320
321    /// A PTY was deleted.
322    #[serde(rename = "pty.deleted")]
323    PtyDeleted {
324        /// Payload.
325        properties: PtyDeletedProps,
326    },
327
328    // ----- worktree -----
329    /// A worktree is ready.
330    #[serde(rename = "worktree.ready")]
331    WorktreeReady {
332        /// Payload.
333        properties: WorktreeReadyProps,
334    },
335
336    /// A worktree operation failed.
337    #[serde(rename = "worktree.failed")]
338    WorktreeFailed {
339        /// Payload.
340        properties: WorktreeFailedProps,
341    },
342}
343
344// ---------------------------------------------------------------------------
345// Property structs
346// ---------------------------------------------------------------------------
347
348/// Empty properties payload for events with no extra data.
349#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
350pub struct EmptyProps {}
351
352/// Properties for [`EventListResponse::InstallationUpdated`].
353#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
354pub struct InstallationUpdatedProps {
355    /// New version string.
356    pub version: String,
357}
358
359/// Properties for [`EventListResponse::InstallationUpdateAvailable`].
360#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
361pub struct InstallationUpdateAvailableProps {
362    /// Available version string.
363    pub version: String,
364}
365
366/// Properties for [`EventListResponse::ProjectUpdated`].
367///
368/// The `Project` type is complex; represented as a JSON value.
369#[allow(clippy::derive_partial_eq_without_eq)]
370#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
371pub struct ProjectUpdatedProps {
372    /// The updated project (complex type, serialised as `serde_json::Value`).
373    pub properties: serde_json::Value,
374}
375
376/// Properties for [`EventListResponse::ServerInstanceDisposed`].
377#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
378pub struct ServerInstanceDisposedProps {
379    /// Directory of the disposed server instance.
380    pub directory: String,
381}
382
383/// Properties for [`EventListResponse::LspClientDiagnostics`].
384#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
385pub struct LspClientDiagnosticsProps {
386    /// File path.
387    pub path: String,
388    /// Language-server identifier.
389    #[serde(rename = "serverID")]
390    pub server_id: String,
391}
392
393/// Properties for [`EventListResponse::MessageUpdated`].
394#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
395pub struct MessageUpdatedProps {
396    /// The updated message.
397    pub info: Message,
398}
399
400/// Properties for [`EventListResponse::MessageRemoved`].
401#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
402pub struct MessageRemovedProps {
403    /// ID of the removed message.
404    #[serde(rename = "messageID")]
405    pub message_id: String,
406    /// Session the message belonged to.
407    #[serde(rename = "sessionID")]
408    pub session_id: String,
409}
410
411/// Properties for [`EventListResponse::MessagePartUpdated`].
412#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
413pub struct MessagePartUpdatedProps {
414    /// The updated part.
415    pub part: Part,
416}
417
418/// Properties for [`EventListResponse::MessagePartDelta`].
419#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
420pub struct MessagePartDeltaProps {
421    /// Session ID.
422    #[serde(rename = "sessionID")]
423    pub session_id: String,
424    /// Message ID.
425    #[serde(rename = "messageID")]
426    pub message_id: String,
427    /// Part ID.
428    #[serde(rename = "partID")]
429    pub part_id: String,
430    /// The field being updated.
431    pub field: String,
432    /// The delta text.
433    pub delta: String,
434}
435
436/// Properties for [`EventListResponse::MessagePartRemoved`].
437#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
438pub struct MessagePartRemovedProps {
439    /// Session the part belonged to.
440    #[serde(rename = "sessionID")]
441    pub session_id: String,
442    /// Message the part belonged to.
443    #[serde(rename = "messageID")]
444    pub message_id: String,
445    /// ID of the removed part.
446    #[serde(rename = "partID")]
447    pub part_id: String,
448}
449
450/// Reply action for a permission request.
451#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
452pub enum PermissionReply {
453    /// Allow once.
454    #[serde(rename = "once")]
455    Once,
456    /// Always allow.
457    #[serde(rename = "always")]
458    Always,
459    /// Reject the permission.
460    #[serde(rename = "reject")]
461    Reject,
462}
463
464/// Properties for [`EventListResponse::PermissionReplied`].
465#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
466pub struct PermissionRepliedProps {
467    /// Session ID.
468    #[serde(rename = "sessionID")]
469    pub session_id: String,
470    /// Request ID.
471    #[serde(rename = "requestID")]
472    pub request_id: String,
473    /// The reply action.
474    pub reply: PermissionReply,
475}
476
477/// Properties for [`EventListResponse::SessionCreated`].
478#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
479pub struct SessionCreatedProps {
480    /// The created session.
481    pub info: Session,
482}
483
484/// Properties for [`EventListResponse::SessionUpdated`].
485#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
486pub struct SessionUpdatedProps {
487    /// The updated session.
488    pub info: Session,
489}
490
491/// Properties for [`EventListResponse::SessionDeleted`].
492#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
493pub struct SessionDeletedProps {
494    /// The deleted session.
495    pub info: Session,
496}
497
498/// Properties for [`EventListResponse::SessionStatus`].
499#[allow(clippy::derive_partial_eq_without_eq)]
500#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
501pub struct SessionStatusProps {
502    /// Session ID.
503    #[serde(rename = "sessionID")]
504    pub session_id: String,
505    /// Session status (complex tagged union, represented as `serde_json::Value`).
506    pub status: serde_json::Value,
507}
508
509/// Properties for [`EventListResponse::SessionIdle`].
510#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
511pub struct SessionIdleProps {
512    /// The idle session's ID.
513    #[serde(rename = "sessionID")]
514    pub session_id: String,
515}
516
517/// Properties for [`EventListResponse::SessionDiff`].
518#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
519pub struct SessionDiffProps {
520    /// Session ID.
521    #[serde(rename = "sessionID")]
522    pub session_id: String,
523    /// The file diffs.
524    pub diff: Vec<FileDiff>,
525}
526
527/// Properties for [`EventListResponse::SessionCompacted`].
528#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
529pub struct SessionCompactedProps {
530    /// Session ID.
531    #[serde(rename = "sessionID")]
532    pub session_id: String,
533}
534
535/// Properties for [`EventListResponse::SessionError`].
536#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
537pub struct SessionErrorProps {
538    /// The error, if any.
539    #[serde(skip_serializing_if = "Option::is_none")]
540    pub error: Option<SessionError>,
541    /// The session ID, if available.
542    #[serde(rename = "sessionID")]
543    #[serde(skip_serializing_if = "Option::is_none")]
544    pub session_id: Option<String>,
545}
546
547/// Properties for [`EventListResponse::QuestionReplied`].
548#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
549pub struct QuestionRepliedProps {
550    /// Session ID.
551    #[serde(rename = "sessionID")]
552    pub session_id: String,
553    /// Request ID.
554    #[serde(rename = "requestID")]
555    pub request_id: String,
556    /// Answers.
557    pub answers: Vec<Vec<String>>,
558}
559
560/// Properties for [`EventListResponse::QuestionRejected`].
561#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
562pub struct QuestionRejectedProps {
563    /// Session ID.
564    #[serde(rename = "sessionID")]
565    pub session_id: String,
566    /// Request ID.
567    #[serde(rename = "requestID")]
568    pub request_id: String,
569}
570
571/// A single to-do item.
572#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
573pub struct Todo {
574    /// To-do content.
575    pub content: String,
576    /// Status of the to-do.
577    pub status: String,
578    /// Priority of the to-do.
579    pub priority: String,
580}
581
582/// Properties for [`EventListResponse::TodoUpdated`].
583#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
584pub struct TodoUpdatedProps {
585    /// Session ID.
586    #[serde(rename = "sessionID")]
587    pub session_id: String,
588    /// The updated to-do list.
589    pub todos: Vec<Todo>,
590}
591
592/// Properties for [`EventListResponse::FileEdited`].
593#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
594pub struct FileEditedProps {
595    /// The edited file path.
596    pub file: String,
597}
598
599/// Kind of file-watcher event.
600#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
601pub enum FileWatcherEvent {
602    /// A file was added.
603    #[serde(rename = "add")]
604    Add,
605    /// A file was changed.
606    #[serde(rename = "change")]
607    Change,
608    /// A file was unlinked (removed).
609    #[serde(rename = "unlink")]
610    Unlink,
611}
612
613/// Properties for [`EventListResponse::FileWatcherUpdated`].
614#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
615pub struct FileWatcherUpdatedProps {
616    /// The kind of file-system event.
617    pub event: FileWatcherEvent,
618    /// The affected file path.
619    pub file: String,
620}
621
622/// Properties for [`EventListResponse::TuiPromptAppend`].
623#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
624pub struct TuiPromptAppendProps {
625    /// Text to append.
626    pub text: String,
627}
628
629/// Properties for [`EventListResponse::TuiCommandExecute`].
630#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
631pub struct TuiCommandExecuteProps {
632    /// Command to execute.
633    pub command: String,
634}
635
636/// Variant for a TUI toast notification.
637#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
638pub enum ToastVariant {
639    /// Informational toast.
640    #[serde(rename = "info")]
641    Info,
642    /// Success toast.
643    #[serde(rename = "success")]
644    Success,
645    /// Warning toast.
646    #[serde(rename = "warning")]
647    Warning,
648    /// Error toast.
649    #[serde(rename = "error")]
650    Error,
651}
652
653/// Properties for [`EventListResponse::TuiToastShow`].
654#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
655pub struct TuiToastShowProps {
656    /// Optional title.
657    #[serde(skip_serializing_if = "Option::is_none")]
658    pub title: Option<String>,
659    /// Toast message.
660    pub message: String,
661    /// Toast variant.
662    pub variant: ToastVariant,
663    /// Optional duration in seconds.
664    #[serde(skip_serializing_if = "Option::is_none")]
665    pub duration: Option<f64>,
666}
667
668/// Properties for [`EventListResponse::TuiSessionSelect`].
669#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
670pub struct TuiSessionSelectProps {
671    /// Session ID to select.
672    #[serde(rename = "sessionID")]
673    pub session_id: String,
674}
675
676/// Properties for [`EventListResponse::McpToolsChanged`].
677#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
678pub struct McpToolsChangedProps {
679    /// MCP server name.
680    pub server: String,
681}
682
683/// Properties for [`EventListResponse::McpBrowserOpenFailed`].
684#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
685pub struct McpBrowserOpenFailedProps {
686    /// MCP name.
687    #[serde(rename = "mcpName")]
688    pub mcp_name: String,
689    /// URL that failed to open.
690    pub url: String,
691}
692
693/// Properties for [`EventListResponse::CommandExecuted`].
694#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
695pub struct CommandExecutedProps {
696    /// Command name.
697    pub name: String,
698    /// Session ID.
699    #[serde(rename = "sessionID")]
700    pub session_id: String,
701    /// Command arguments.
702    pub arguments: String,
703    /// Message ID.
704    #[serde(rename = "messageID")]
705    pub message_id: String,
706}
707
708/// Properties for [`EventListResponse::VcsBranchUpdated`].
709#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
710pub struct VcsBranchUpdatedProps {
711    /// Branch name, if available.
712    #[serde(skip_serializing_if = "Option::is_none")]
713    pub branch: Option<String>,
714}
715
716/// Status of a PTY process.
717#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
718pub enum PtyStatus {
719    /// The PTY is running.
720    #[serde(rename = "running")]
721    Running,
722    /// The PTY has exited.
723    #[serde(rename = "exited")]
724    Exited,
725}
726
727/// A pseudo-terminal (PTY) descriptor.
728#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
729pub struct Pty {
730    /// PTY identifier.
731    pub id: String,
732    /// PTY title.
733    pub title: String,
734    /// Command being run.
735    pub command: String,
736    /// Command arguments.
737    pub args: Vec<String>,
738    /// Working directory.
739    pub cwd: String,
740    /// PTY status.
741    pub status: PtyStatus,
742    /// Process ID.
743    pub pid: f64,
744}
745
746/// Properties for [`EventListResponse::PtyCreated`].
747#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
748pub struct PtyCreatedProps {
749    /// PTY info.
750    pub info: Pty,
751}
752
753/// Properties for [`EventListResponse::PtyUpdated`].
754#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
755pub struct PtyUpdatedProps {
756    /// PTY info.
757    pub info: Pty,
758}
759
760/// Properties for [`EventListResponse::PtyExited`].
761#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
762pub struct PtyExitedProps {
763    /// PTY identifier.
764    pub id: String,
765    /// Exit code.
766    #[serde(rename = "exitCode")]
767    pub exit_code: f64,
768}
769
770/// Properties for [`EventListResponse::PtyDeleted`].
771#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
772pub struct PtyDeletedProps {
773    /// PTY identifier.
774    pub id: String,
775}
776
777/// Properties for [`EventListResponse::WorktreeReady`].
778#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
779pub struct WorktreeReadyProps {
780    /// Worktree name.
781    pub name: String,
782    /// Branch name.
783    pub branch: String,
784}
785
786/// Properties for [`EventListResponse::WorktreeFailed`].
787#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
788pub struct WorktreeFailedProps {
789    /// Error message.
790    pub message: String,
791}
792
793// ---------------------------------------------------------------------------
794// EventResource
795// ---------------------------------------------------------------------------
796
797/// Resource accessor for the `/event` SSE endpoint.
798pub struct EventResource<'a> {
799    client: &'a Opencode,
800}
801
802impl<'a> EventResource<'a> {
803    /// Create a new `EventResource` bound to the given client.
804    pub(crate) const fn new(client: &'a Opencode) -> Self {
805        Self { client }
806    }
807
808    /// List events as an SSE stream.
809    ///
810    /// The `/event` endpoint returns a Server-Sent Events stream where
811    /// each event's `data` field is a JSON-encoded [`EventListResponse`].
812    pub async fn list(
813        &self,
814    ) -> Result<crate::streaming::SseStream<EventListResponse>, crate::error::OpencodeError> {
815        self.client.get_stream("/event").await
816    }
817}
818
819// ---------------------------------------------------------------------------
820// Tests
821// ---------------------------------------------------------------------------
822
823#[cfg(test)]
824mod tests {
825    use super::*;
826    use crate::resources::session::{UserMessage, UserMessageModel, UserMessageTime};
827
828    // -- InstallationUpdated round-trip --
829
830    #[test]
831    fn installation_updated_round_trip() {
832        let event = EventListResponse::InstallationUpdated {
833            properties: InstallationUpdatedProps { version: "1.2.3".into() },
834        };
835        let json_str = serde_json::to_string(&event).unwrap();
836        assert!(json_str.contains(r#""type":"installation.updated"#));
837        assert!(json_str.contains(r#""version":"1.2.3"#));
838        let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
839        assert_eq!(event, back);
840    }
841
842    // -- MessageUpdated round-trip (with full Message) --
843
844    #[test]
845    fn message_updated_round_trip() {
846        let msg = Message::User(Box::new(UserMessage {
847            id: "msg_u001".into(),
848            session_id: "sess_001".into(),
849            time: UserMessageTime { created: 1_700_000_100.0 },
850            agent: "coder".into(),
851            model: UserMessageModel { provider_id: "openai".into(), model_id: "gpt-4o".into() },
852            format: None,
853            summary: None,
854            system: None,
855            tools: None,
856            variant: None,
857        }));
858
859        let event = EventListResponse::MessageUpdated {
860            properties: MessageUpdatedProps { info: msg.clone() },
861        };
862        let json_str = serde_json::to_string(&event).unwrap();
863        assert!(json_str.contains(r#""type":"message.updated"#));
864        assert!(json_str.contains(r#""role":"user"#));
865        let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
866        assert_eq!(event, back);
867    }
868
869    // -- SessionError round-trip --
870
871    #[test]
872    fn session_error_round_trip() {
873        use crate::resources::shared::{SessionError as SE, UnknownErrorData};
874
875        let event = EventListResponse::SessionError {
876            properties: SessionErrorProps {
877                error: Some(SE::UnknownError {
878                    data: UnknownErrorData { message: "something broke".into() },
879                }),
880                session_id: Some("sess_err_001".into()),
881            },
882        };
883        let json_str = serde_json::to_string(&event).unwrap();
884        assert!(json_str.contains(r#""type":"session.error"#));
885        assert!(json_str.contains(r#""name":"UnknownError"#));
886        assert!(json_str.contains("something broke"));
887        let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
888        assert_eq!(event, back);
889    }
890
891    #[test]
892    fn session_error_empty_round_trip() {
893        let event = EventListResponse::SessionError {
894            properties: SessionErrorProps { error: None, session_id: None },
895        };
896        let json_str = serde_json::to_string(&event).unwrap();
897        // Optional fields should be omitted (check for the key, not substring)
898        assert!(!json_str.contains(r#""error""#));
899        assert!(!json_str.contains(r#""sessionID""#));
900        let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
901        assert_eq!(event, back);
902    }
903
904    // -- FileWatcherUpdated round-trip --
905
906    #[test]
907    fn file_watcher_updated_round_trip() {
908        let event = EventListResponse::FileWatcherUpdated {
909            properties: FileWatcherUpdatedProps {
910                event: FileWatcherEvent::Add,
911                file: "src/main.rs".into(),
912            },
913        };
914        let json_str = serde_json::to_string(&event).unwrap();
915        assert!(json_str.contains(r#""type":"file.watcher.updated"#));
916        assert!(json_str.contains(r#""event":"add"#));
917        let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
918        assert_eq!(event, back);
919
920        // Also test the Change variant
921        let event2 = EventListResponse::FileWatcherUpdated {
922            properties: FileWatcherUpdatedProps {
923                event: FileWatcherEvent::Change,
924                file: "Cargo.toml".into(),
925            },
926        };
927        let json_str2 = serde_json::to_string(&event2).unwrap();
928        assert!(json_str2.contains(r#""event":"change"#));
929        let back2: EventListResponse = serde_json::from_str(&json_str2).unwrap();
930        assert_eq!(event2, back2);
931
932        // Also test the Unlink variant
933        let event3 = EventListResponse::FileWatcherUpdated {
934            properties: FileWatcherUpdatedProps {
935                event: FileWatcherEvent::Unlink,
936                file: "old_file.rs".into(),
937            },
938        };
939        let json_str3 = serde_json::to_string(&event3).unwrap();
940        assert!(json_str3.contains(r#""event":"unlink"#));
941        let back3: EventListResponse = serde_json::from_str(&json_str3).unwrap();
942        assert_eq!(event3, back3);
943    }
944
945    // -- Deserialization from raw JSON --
946
947    #[test]
948    fn deserialize_permission_asked() {
949        let raw = r#"{
950            "type": "permission.asked",
951            "properties": {
952                "id": "perm_001",
953                "sessionID": "sess_001",
954                "title": "Run bash command"
955            }
956        }"#;
957        let event: EventListResponse = serde_json::from_str(raw).unwrap();
958        match &event {
959            EventListResponse::PermissionAsked { properties } => {
960                assert_eq!(properties["id"], "perm_001");
961                assert_eq!(properties["sessionID"], "sess_001");
962            }
963            other => panic!("expected PermissionAsked, got {other:?}"),
964        }
965        // Round-trip
966        let json_str = serde_json::to_string(&event).unwrap();
967        let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
968        assert_eq!(event, back);
969    }
970
971    #[test]
972    fn deserialize_permission_replied() {
973        let raw = r#"{
974            "type": "permission.replied",
975            "properties": {
976                "sessionID": "sess_001",
977                "requestID": "req_001",
978                "reply": "always"
979            }
980        }"#;
981        let event: EventListResponse = serde_json::from_str(raw).unwrap();
982        match &event {
983            EventListResponse::PermissionReplied { properties } => {
984                assert_eq!(properties.session_id, "sess_001");
985                assert_eq!(properties.request_id, "req_001");
986                assert_eq!(properties.reply, PermissionReply::Always);
987            }
988            other => panic!("expected PermissionReplied, got {other:?}"),
989        }
990    }
991
992    // -- Missing event variant round-trips --
993
994    #[test]
995    fn lsp_client_diagnostics_round_trip() {
996        let event = EventListResponse::LspClientDiagnostics {
997            properties: LspClientDiagnosticsProps {
998                path: "src/main.rs".into(),
999                server_id: "rust-analyzer".into(),
1000            },
1001        };
1002        let json_str = serde_json::to_string(&event).unwrap();
1003        assert!(json_str.contains(r#""type":"lsp.client.diagnostics"#));
1004        assert!(json_str.contains(r#""serverID":"rust-analyzer"#));
1005        let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1006        assert_eq!(event, back);
1007    }
1008
1009    #[test]
1010    fn message_removed_round_trip() {
1011        let event = EventListResponse::MessageRemoved {
1012            properties: MessageRemovedProps {
1013                message_id: "msg_del_001".into(),
1014                session_id: "sess_001".into(),
1015            },
1016        };
1017        let json_str = serde_json::to_string(&event).unwrap();
1018        assert!(json_str.contains(r#""type":"message.removed"#));
1019        assert!(json_str.contains("messageID"));
1020        assert!(json_str.contains("sessionID"));
1021        let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1022        assert_eq!(event, back);
1023    }
1024
1025    #[test]
1026    fn message_part_updated_round_trip() {
1027        use crate::resources::session::{Part, TextPart};
1028
1029        let event = EventListResponse::MessagePartUpdated {
1030            properties: MessagePartUpdatedProps {
1031                part: Part::Text(TextPart {
1032                    id: "p_upd_001".into(),
1033                    message_id: "msg_001".into(),
1034                    session_id: "sess_001".into(),
1035                    text: "updated text".into(),
1036                    synthetic: None,
1037                    time: None,
1038                }),
1039            },
1040        };
1041        let json_str = serde_json::to_string(&event).unwrap();
1042        assert!(json_str.contains(r#""type":"message.part.updated"#));
1043        let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1044        assert_eq!(event, back);
1045    }
1046
1047    #[test]
1048    fn message_part_removed_round_trip() {
1049        let event = EventListResponse::MessagePartRemoved {
1050            properties: MessagePartRemovedProps {
1051                session_id: "sess_001".into(),
1052                message_id: "msg_001".into(),
1053                part_id: "p_del_001".into(),
1054            },
1055        };
1056        let json_str = serde_json::to_string(&event).unwrap();
1057        assert!(json_str.contains(r#""type":"message.part.removed"#));
1058        assert!(json_str.contains("sessionID"));
1059        assert!(json_str.contains("messageID"));
1060        assert!(json_str.contains("partID"));
1061        let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1062        assert_eq!(event, back);
1063    }
1064
1065    #[test]
1066    fn file_edited_round_trip() {
1067        let event = EventListResponse::FileEdited {
1068            properties: FileEditedProps { file: "src/lib.rs".into() },
1069        };
1070        let json_str = serde_json::to_string(&event).unwrap();
1071        assert!(json_str.contains(r#""type":"file.edited"#));
1072        let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1073        assert_eq!(event, back);
1074    }
1075
1076    #[test]
1077    fn session_updated_round_trip() {
1078        let event = EventListResponse::SessionUpdated {
1079            properties: SessionUpdatedProps {
1080                info: Session {
1081                    id: "sess_upd".into(),
1082                    slug: String::new(),
1083                    project_id: String::new(),
1084                    directory: String::new(),
1085                    time: crate::resources::session::SessionTime {
1086                        created: 1_700_000_000.0,
1087                        updated: 1_700_001_000.0,
1088                        compacting: None,
1089                        archived: None,
1090                    },
1091                    title: "Updated".into(),
1092                    version: "1".into(),
1093                    parent_id: None,
1094                    revert: None,
1095                    share: None,
1096                    summary: None,
1097                    permission: None,
1098                },
1099            },
1100        };
1101        let json_str = serde_json::to_string(&event).unwrap();
1102        assert!(json_str.contains(r#""type":"session.updated"#));
1103        let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1104        assert_eq!(event, back);
1105    }
1106
1107    #[test]
1108    fn session_deleted_round_trip() {
1109        let event = EventListResponse::SessionDeleted {
1110            properties: SessionDeletedProps {
1111                info: Session {
1112                    id: "sess_del".into(),
1113                    slug: String::new(),
1114                    project_id: String::new(),
1115                    directory: String::new(),
1116                    time: crate::resources::session::SessionTime {
1117                        created: 1_700_000_000.0,
1118                        updated: 1_700_000_000.0,
1119                        compacting: None,
1120                        archived: None,
1121                    },
1122                    title: "Deleted".into(),
1123                    version: "1".into(),
1124                    parent_id: None,
1125                    revert: None,
1126                    share: None,
1127                    summary: None,
1128                    permission: None,
1129                },
1130            },
1131        };
1132        let json_str = serde_json::to_string(&event).unwrap();
1133        assert!(json_str.contains(r#""type":"session.deleted"#));
1134        let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1135        assert_eq!(event, back);
1136    }
1137
1138    #[test]
1139    fn session_idle_round_trip() {
1140        let event = EventListResponse::SessionIdle {
1141            properties: SessionIdleProps { session_id: "sess_idle_001".into() },
1142        };
1143        let json_str = serde_json::to_string(&event).unwrap();
1144        assert!(json_str.contains(r#""type":"session.idle"#));
1145        assert!(json_str.contains("sessionID"));
1146        let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1147        assert_eq!(event, back);
1148    }
1149
1150    #[test]
1151    fn session_error_both_fields_null() {
1152        // Deserialize from JSON where both fields are explicitly null
1153        let raw = r#"{
1154            "type": "session.error",
1155            "properties": { "error": null, "sessionID": null }
1156        }"#;
1157        let event: EventListResponse = serde_json::from_str(raw).unwrap();
1158        match &event {
1159            EventListResponse::SessionError { properties } => {
1160                assert_eq!(properties.error, None);
1161                assert_eq!(properties.session_id, None);
1162            }
1163            other => panic!("expected SessionError, got {other:?}"),
1164        }
1165    }
1166
1167    // -- New event variant tests --
1168
1169    #[test]
1170    fn installation_update_available_round_trip() {
1171        let event = EventListResponse::InstallationUpdateAvailable {
1172            properties: InstallationUpdateAvailableProps { version: "2.0.0".into() },
1173        };
1174        let json_str = serde_json::to_string(&event).unwrap();
1175        assert!(json_str.contains(r#""type":"installation.update-available"#));
1176        assert!(json_str.contains(r#""version":"2.0.0"#));
1177        let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1178        assert_eq!(event, back);
1179    }
1180
1181    #[test]
1182    fn message_part_delta_round_trip() {
1183        let event = EventListResponse::MessagePartDelta {
1184            properties: MessagePartDeltaProps {
1185                session_id: "sess_001".into(),
1186                message_id: "msg_001".into(),
1187                part_id: "part_001".into(),
1188                field: "text".into(),
1189                delta: "hello ".into(),
1190            },
1191        };
1192        let json_str = serde_json::to_string(&event).unwrap();
1193        assert!(json_str.contains(r#""type":"message.part.delta"#));
1194        assert!(json_str.contains(r#""sessionID":"sess_001"#));
1195        assert!(json_str.contains(r#""delta":"hello "#));
1196        let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1197        assert_eq!(event, back);
1198    }
1199
1200    #[test]
1201    fn server_connected_round_trip() {
1202        let event = EventListResponse::ServerConnected { properties: EmptyProps {} };
1203        let json_str = serde_json::to_string(&event).unwrap();
1204        assert!(json_str.contains(r#""type":"server.connected"#));
1205        let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1206        assert_eq!(event, back);
1207    }
1208
1209    #[test]
1210    fn tui_toast_show_round_trip() {
1211        let event = EventListResponse::TuiToastShow {
1212            properties: TuiToastShowProps {
1213                title: Some("Heads up".into()),
1214                message: "Build succeeded".into(),
1215                variant: ToastVariant::Success,
1216                duration: Some(5.0),
1217            },
1218        };
1219        let json_str = serde_json::to_string(&event).unwrap();
1220        assert!(json_str.contains(r#""type":"tui.toast.show"#));
1221        assert!(json_str.contains(r#""variant":"success"#));
1222        assert!(json_str.contains(r#""title":"Heads up"#));
1223        let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1224        assert_eq!(event, back);
1225
1226        // Without optional fields
1227        let event2 = EventListResponse::TuiToastShow {
1228            properties: TuiToastShowProps {
1229                title: None,
1230                message: "Error occurred".into(),
1231                variant: ToastVariant::Error,
1232                duration: None,
1233            },
1234        };
1235        let json_str2 = serde_json::to_string(&event2).unwrap();
1236        assert!(!json_str2.contains(r#""title""#));
1237        assert!(!json_str2.contains(r#""duration""#));
1238        let back2: EventListResponse = serde_json::from_str(&json_str2).unwrap();
1239        assert_eq!(event2, back2);
1240    }
1241
1242    #[test]
1243    fn todo_updated_round_trip() {
1244        let event = EventListResponse::TodoUpdated {
1245            properties: TodoUpdatedProps {
1246                session_id: "sess_001".into(),
1247                todos: vec![
1248                    Todo {
1249                        content: "Fix bug".into(),
1250                        status: "pending".into(),
1251                        priority: "high".into(),
1252                    },
1253                    Todo {
1254                        content: "Write docs".into(),
1255                        status: "done".into(),
1256                        priority: "low".into(),
1257                    },
1258                ],
1259            },
1260        };
1261        let json_str = serde_json::to_string(&event).unwrap();
1262        assert!(json_str.contains(r#""type":"todo.updated"#));
1263        assert!(json_str.contains("Fix bug"));
1264        let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1265        assert_eq!(event, back);
1266    }
1267
1268    #[test]
1269    fn worktree_ready_round_trip() {
1270        let event = EventListResponse::WorktreeReady {
1271            properties: WorktreeReadyProps {
1272                name: "feature-branch".into(),
1273                branch: "feat/new-feature".into(),
1274            },
1275        };
1276        let json_str = serde_json::to_string(&event).unwrap();
1277        assert!(json_str.contains(r#""type":"worktree.ready"#));
1278        assert!(json_str.contains(r#""name":"feature-branch"#));
1279        let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1280        assert_eq!(event, back);
1281    }
1282
1283    #[test]
1284    fn question_replied_round_trip() {
1285        let event = EventListResponse::QuestionReplied {
1286            properties: QuestionRepliedProps {
1287                session_id: "sess_001".into(),
1288                request_id: "req_001".into(),
1289                answers: vec![vec!["yes".into(), "no".into()]],
1290            },
1291        };
1292        let json_str = serde_json::to_string(&event).unwrap();
1293        assert!(json_str.contains(r#""type":"question.replied"#));
1294        assert!(json_str.contains(r#""sessionID":"sess_001"#));
1295        let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1296        assert_eq!(event, back);
1297    }
1298
1299    #[test]
1300    fn mcp_tools_changed_round_trip() {
1301        let event = EventListResponse::McpToolsChanged {
1302            properties: McpToolsChangedProps { server: "my-mcp-server".into() },
1303        };
1304        let json_str = serde_json::to_string(&event).unwrap();
1305        assert!(json_str.contains(r#""type":"mcp.tools.changed"#));
1306        assert!(json_str.contains(r#""server":"my-mcp-server"#));
1307        let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1308        assert_eq!(event, back);
1309    }
1310
1311    #[test]
1312    fn pty_created_round_trip() {
1313        let event = EventListResponse::PtyCreated {
1314            properties: PtyCreatedProps {
1315                info: Pty {
1316                    id: "pty_001".into(),
1317                    title: "shell".into(),
1318                    command: "/bin/zsh".into(),
1319                    args: vec!["-l".into()],
1320                    cwd: "/home/user".into(),
1321                    status: PtyStatus::Running,
1322                    pid: 12345.0,
1323                },
1324            },
1325        };
1326        let json_str = serde_json::to_string(&event).unwrap();
1327        assert!(json_str.contains(r#""type":"pty.created"#));
1328        assert!(json_str.contains(r#""status":"running"#));
1329        let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1330        assert_eq!(event, back);
1331    }
1332
1333    #[test]
1334    fn vcs_branch_updated_round_trip() {
1335        let event = EventListResponse::VcsBranchUpdated {
1336            properties: VcsBranchUpdatedProps { branch: Some("main".into()) },
1337        };
1338        let json_str = serde_json::to_string(&event).unwrap();
1339        assert!(json_str.contains(r#""type":"vcs.branch.updated"#));
1340        assert!(json_str.contains(r#""branch":"main"#));
1341        let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1342        assert_eq!(event, back);
1343
1344        // Branch is optional
1345        let event2 = EventListResponse::VcsBranchUpdated {
1346            properties: VcsBranchUpdatedProps { branch: None },
1347        };
1348        let json_str2 = serde_json::to_string(&event2).unwrap();
1349        assert!(!json_str2.contains(r#""branch""#));
1350        let back2: EventListResponse = serde_json::from_str(&json_str2).unwrap();
1351        assert_eq!(event2, back2);
1352    }
1353
1354    #[test]
1355    fn command_executed_round_trip() {
1356        let event = EventListResponse::CommandExecuted {
1357            properties: CommandExecutedProps {
1358                name: "test-cmd".into(),
1359                session_id: "sess_001".into(),
1360                arguments: "{}".into(),
1361                message_id: "msg_001".into(),
1362            },
1363        };
1364        let json_str = serde_json::to_string(&event).unwrap();
1365        assert!(json_str.contains(r#""type":"command.executed"#));
1366        assert!(json_str.contains(r#""sessionID":"sess_001"#));
1367        assert!(json_str.contains(r#""messageID":"msg_001"#));
1368        let back: EventListResponse = serde_json::from_str(&json_str).unwrap();
1369        assert_eq!(event, back);
1370    }
1371
1372    #[test]
1373    fn deserialize_project_updated_from_raw() {
1374        let raw = r#"{
1375            "type": "project.updated",
1376            "properties": {
1377                "properties": { "name": "my-project", "path": "/tmp/proj" }
1378            }
1379        }"#;
1380        let event: EventListResponse = serde_json::from_str(raw).unwrap();
1381        match &event {
1382            EventListResponse::ProjectUpdated { properties } => {
1383                assert_eq!(properties.properties["name"], "my-project");
1384            }
1385            other => panic!("expected ProjectUpdated, got {other:?}"),
1386        }
1387    }
1388
1389    #[test]
1390    fn deserialize_session_status_from_raw() {
1391        let raw = r#"{
1392            "type": "session.status",
1393            "properties": {
1394                "sessionID": "sess_001",
1395                "status": { "type": "running", "tool": "bash" }
1396            }
1397        }"#;
1398        let event: EventListResponse = serde_json::from_str(raw).unwrap();
1399        match &event {
1400            EventListResponse::SessionStatus { properties } => {
1401                assert_eq!(properties.session_id, "sess_001");
1402                assert_eq!(properties.status["type"], "running");
1403            }
1404            other => panic!("expected SessionStatus, got {other:?}"),
1405        }
1406    }
1407}