use crate::types::error::APIError;
use crate::types::permission::{PermissionReply, PermissionRequest};
use crate::types::session::Session;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GlobalEventEnvelope {
pub directory: String,
pub payload: Event,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Event {
#[serde(rename = "server.connected")]
ServerConnected {
#[serde(default)]
properties: serde_json::Value,
},
#[serde(rename = "server.heartbeat")]
ServerHeartbeat {
#[serde(default)]
properties: serde_json::Value,
},
#[serde(rename = "server.instance.disposed")]
ServerInstanceDisposed {
#[serde(default)]
properties: serde_json::Value,
},
#[serde(rename = "global.disposed")]
GlobalDisposed {
#[serde(default)]
properties: serde_json::Value,
},
#[serde(rename = "session.created")]
SessionCreated {
properties: SessionInfoProps,
},
#[serde(rename = "session.updated")]
SessionUpdated {
properties: SessionInfoProps,
},
#[serde(rename = "session.deleted")]
SessionDeleted {
properties: SessionInfoProps,
},
#[serde(rename = "session.diff")]
SessionDiff {
#[serde(default)]
properties: serde_json::Value,
},
#[serde(rename = "session.error")]
SessionError {
properties: SessionErrorProps,
},
#[serde(rename = "session.compacted")]
SessionCompacted {
#[serde(default)]
properties: serde_json::Value,
},
#[serde(rename = "session.status")]
SessionStatus {
#[serde(default)]
properties: serde_json::Value,
},
#[serde(rename = "session.idle")]
SessionIdle {
properties: SessionIdleProps,
},
#[serde(rename = "message.updated")]
MessageUpdated {
properties: MessageUpdatedProps,
},
#[serde(rename = "message.removed")]
MessageRemoved {
properties: MessageRemovedProps,
},
#[serde(rename = "message.part.updated")]
MessagePartUpdated {
properties: Box<MessagePartEventProps>,
},
#[serde(rename = "message.part.removed")]
MessagePartRemoved {
#[serde(default)]
properties: serde_json::Value,
},
#[serde(rename = "pty.created")]
PtyCreated {
#[serde(default)]
properties: serde_json::Value,
},
#[serde(rename = "pty.updated")]
PtyUpdated {
#[serde(default)]
properties: serde_json::Value,
},
#[serde(rename = "pty.exited")]
PtyExited {
#[serde(default)]
properties: serde_json::Value,
},
#[serde(rename = "pty.deleted")]
PtyDeleted {
#[serde(default)]
properties: serde_json::Value,
},
#[serde(rename = "permission.updated")]
PermissionUpdated {
#[serde(default)]
properties: serde_json::Value,
},
#[serde(rename = "permission.replied")]
PermissionReplied {
properties: PermissionRepliedProps,
},
#[serde(rename = "permission.asked")]
PermissionAsked {
properties: PermissionAskedProps,
},
#[serde(rename = "permission.replied-next")]
PermissionRepliedNext {
properties: PermissionRepliedProps,
},
#[serde(rename = "project.updated")]
ProjectUpdated {
#[serde(default)]
properties: serde_json::Value,
},
#[serde(rename = "file.edited")]
FileEdited {
#[serde(default)]
properties: serde_json::Value,
},
#[serde(rename = "file.watcher.updated")]
FileWatcherUpdated {
#[serde(default)]
properties: serde_json::Value,
},
#[serde(rename = "vcs.branch.updated")]
VcsBranchUpdated {
#[serde(default)]
properties: serde_json::Value,
},
#[serde(rename = "lsp.updated")]
LspUpdated {
#[serde(default)]
properties: serde_json::Value,
},
#[serde(rename = "lsp.client.diagnostics")]
LspClientDiagnostics {
#[serde(default)]
properties: serde_json::Value,
},
#[serde(rename = "command.executed")]
CommandExecuted {
#[serde(default)]
properties: serde_json::Value,
},
#[serde(rename = "mcp.tools.changed")]
McpToolsChanged {
#[serde(default)]
properties: serde_json::Value,
},
#[serde(rename = "installation.updated")]
InstallationUpdated {
#[serde(default)]
properties: serde_json::Value,
},
#[serde(rename = "installation.update-available")]
InstallationUpdateAvailable {
#[serde(default)]
properties: serde_json::Value,
},
#[serde(rename = "ide.installed")]
IdeInstalled {
#[serde(default)]
properties: serde_json::Value,
},
#[serde(rename = "tui.prompt.append")]
TuiPromptAppend {
#[serde(default)]
properties: serde_json::Value,
},
#[serde(rename = "tui.command.execute")]
TuiCommandExecute {
#[serde(default)]
properties: serde_json::Value,
},
#[serde(rename = "tui.toast.show")]
TuiToastShow {
#[serde(default)]
properties: serde_json::Value,
},
#[serde(rename = "tui.session.select")]
TuiSessionSelect {
#[serde(default)]
properties: serde_json::Value,
},
#[serde(rename = "todo.updated")]
TodoUpdated {
#[serde(default)]
properties: serde_json::Value,
},
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionInfoProps {
pub info: Session,
#[serde(flatten)]
pub extra: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionIdleProps {
#[serde(default, alias = "sessionID")]
pub session_id: Option<String>,
#[serde(flatten)]
pub extra: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum AssistantError {
Api(APIError),
Unknown(serde_json::Value),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SessionErrorProps {
#[serde(default, alias = "sessionID")]
pub session_id: Option<String>,
#[serde(default)]
pub error: Option<AssistantError>,
#[serde(flatten)]
pub extra: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MessageUpdatedProps {
pub info: crate::types::message::MessageInfo,
#[serde(flatten)]
pub extra: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MessageRemovedProps {
#[serde(alias = "sessionID")]
pub session_id: String,
pub message_id: String,
#[serde(flatten)]
pub extra: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MessagePartEventProps {
#[serde(default, alias = "sessionID")]
pub session_id: Option<String>,
#[serde(default)]
pub message_id: Option<String>,
#[serde(default)]
pub index: Option<usize>,
#[serde(default)]
pub part: Option<crate::types::message::Part>,
#[serde(default)]
pub delta: Option<String>,
#[serde(flatten)]
pub extra: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PermissionAskedProps {
#[serde(flatten)]
pub request: PermissionRequest,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PermissionRepliedProps {
#[serde(alias = "sessionID")]
pub session_id: String,
pub request_id: String,
pub reply: PermissionReply,
#[serde(flatten)]
pub extra: serde_json::Value,
}
impl Event {
pub fn session_id(&self) -> Option<&str> {
match self {
Event::SessionCreated { properties } => Some(&properties.info.id),
Event::SessionUpdated { properties } => Some(&properties.info.id),
Event::SessionDeleted { properties } => Some(&properties.info.id),
Event::SessionIdle { properties } => properties.session_id.as_deref(),
Event::SessionError { properties } => properties.session_id.as_deref(),
Event::MessageUpdated { properties } => properties.info.session_id.as_deref(),
Event::MessageRemoved { properties } => Some(&properties.session_id),
Event::MessagePartUpdated { properties } => properties.session_id.as_deref(),
Event::PermissionAsked { properties } => properties.request.session_id.as_deref(),
Event::PermissionReplied { properties } => Some(&properties.session_id),
Event::PermissionRepliedNext { properties } => Some(&properties.session_id),
_ => None,
}
}
pub fn is_heartbeat(&self) -> bool {
matches!(self, Event::ServerHeartbeat { .. })
}
pub fn is_connected(&self) -> bool {
matches!(self, Event::ServerConnected { .. })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_event_deserialize_session_created() {
let json = r#"{
"type": "session.created",
"properties": {
"info": {
"id": "sess-123",
"title": "Test Session",
"version": "1.0"
}
}
}"#;
let event: Event = serde_json::from_str(json).unwrap();
assert!(matches!(event, Event::SessionCreated { .. }));
assert_eq!(event.session_id(), Some("sess-123"));
}
#[test]
fn test_event_deserialize_heartbeat() {
let json = r#"{"type":"server.heartbeat","properties":{}}"#;
let event: Event = serde_json::from_str(json).unwrap();
assert!(matches!(event, Event::ServerHeartbeat { .. }));
assert!(event.is_heartbeat());
}
#[test]
fn test_event_deserialize_unknown() {
let json = r#"{"type":"some.future.event","properties":{}}"#;
let event: Event = serde_json::from_str(json).unwrap();
assert!(matches!(event, Event::Unknown));
}
#[test]
fn test_message_part_with_delta() {
let json = r#"{"type":"message.part.updated","properties":{"sessionId":"s1","messageId":"m1","delta":"Hello"}}"#;
let event: Event = serde_json::from_str(json).unwrap();
if let Event::MessagePartUpdated { properties } = &event {
assert_eq!(properties.delta, Some("Hello".to_string()));
} else {
panic!("Expected MessagePartUpdated");
}
}
#[test]
fn test_event_deserialize_pty_created() {
let json = r#"{"type":"pty.created","properties":{"id":"pty1"}}"#;
let event: Event = serde_json::from_str(json).unwrap();
assert!(matches!(event, Event::PtyCreated { .. }));
}
#[test]
fn test_event_deserialize_permission_asked() {
let json = r#"{
"type": "permission.asked",
"properties": {
"id": "req-123",
"sessionId": "sess-456",
"permission": "file.write",
"patterns": ["**/*.rs"]
}
}"#;
let event: Event = serde_json::from_str(json).unwrap();
assert!(matches!(event, Event::PermissionAsked { .. }));
assert_eq!(event.session_id(), Some("sess-456"));
}
#[test]
fn test_event_deserialize_permission_replied() {
let json = r#"{
"type": "permission.replied",
"properties": {
"sessionId": "sess-456",
"requestId": "req-123",
"reply": "always"
}
}"#;
let event: Event = serde_json::from_str(json).unwrap();
assert!(matches!(event, Event::PermissionReplied { .. }));
assert_eq!(event.session_id(), Some("sess-456"));
}
#[test]
fn test_event_deserialize_message_updated() {
let json = r#"{
"type": "message.updated",
"properties": {
"info": {
"id": "msg-123",
"sessionId": "sess-456",
"role": "assistant",
"time": {"created": 1234567890}
}
}
}"#;
let event: Event = serde_json::from_str(json).unwrap();
assert!(matches!(event, Event::MessageUpdated { .. }));
assert_eq!(event.session_id(), Some("sess-456"));
}
#[test]
fn test_event_deserialize_message_removed() {
let json = r#"{
"type": "message.removed",
"properties": {
"sessionId": "sess-456",
"messageId": "msg-123"
}
}"#;
let event: Event = serde_json::from_str(json).unwrap();
assert!(matches!(event, Event::MessageRemoved { .. }));
assert_eq!(event.session_id(), Some("sess-456"));
}
#[test]
fn test_event_deserialize_session_error() {
let json = r#"{
"type": "session.error",
"properties": {
"sessionId": "sess-456",
"error": {"message": "Something went wrong", "isRetryable": false}
}
}"#;
let event: Event = serde_json::from_str(json).unwrap();
if let Event::SessionError { properties } = &event {
assert!(properties.error.is_some());
if let Some(AssistantError::Api(err)) = &properties.error {
assert_eq!(err.message, "Something went wrong");
} else {
panic!("Expected APIError");
}
} else {
panic!("Expected SessionError");
}
}
#[test]
fn test_event_deserialize_session_idle_with_session_id_alias() {
let json = r#"{
"type": "session.idle",
"properties": {
"sessionID": "sess-456"
}
}"#;
let event: Event = serde_json::from_str(json).unwrap();
assert!(matches!(event, Event::SessionIdle { .. }));
assert_eq!(event.session_id(), Some("sess-456"));
}
#[test]
fn test_event_deserialize_message_removed_with_session_id_alias() {
let json = r#"{
"type": "message.removed",
"properties": {
"sessionID": "sess-456",
"messageId": "msg-123"
}
}"#;
let event: Event = serde_json::from_str(json).unwrap();
assert!(matches!(event, Event::MessageRemoved { .. }));
assert_eq!(event.session_id(), Some("sess-456"));
}
#[test]
fn test_event_deserialize_todo_updated() {
let json = r#"{"type":"todo.updated","properties":{}}"#;
let event: Event = serde_json::from_str(json).unwrap();
assert!(matches!(event, Event::TodoUpdated { .. }));
}
}