use crate::types::error::APIError;
use crate::types::permission::PermissionReply;
use crate::types::permission::PermissionRequest;
use crate::types::session::Session;
use serde::Deserialize;
use serde::Serialize;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GlobalEventEnvelope {
pub directory: String,
pub payload: Event,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
#[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: Box<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 = "mcp.browser.open.failed")]
McpBrowserOpenFailed {
#[serde(default)]
properties: serde_json::Value,
},
#[serde(rename = "workspace.ready")]
WorkspaceReady {
#[serde(default)]
properties: serde_json::Value,
},
#[serde(rename = "workspace.failed")]
WorkspaceFailed {
#[serde(default)]
properties: serde_json::Value,
},
#[serde(rename = "worktree.ready")]
WorktreeReady {
#[serde(default)]
properties: serde_json::Value,
},
#[serde(rename = "worktree.failed")]
WorktreeFailed {
#[serde(default)]
properties: serde_json::Value,
},
#[serde(rename = "message.part.delta")]
MessagePartDelta {
properties: Box<MessagePartEventProps>,
},
#[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 = "question.asked")]
QuestionAsked {
properties: QuestionAskedProps,
},
#[serde(rename = "question.replied")]
QuestionReplied {
properties: QuestionRepliedProps,
},
#[serde(rename = "question.rejected")]
QuestionRejected {
properties: QuestionRejectedProps,
},
#[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)]
pub struct SessionIdleProps {
#[serde(rename = "sessionID")]
pub session_id: 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)]
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 {
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)]
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 {
pub session_id: String,
pub request_id: String,
pub reply: PermissionReply,
#[serde(flatten)]
pub extra: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QuestionAskedProps {
#[serde(flatten)]
pub request: crate::types::question::QuestionRequest,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QuestionRepliedProps {
pub session_id: String,
pub request_id: String,
pub answers: Vec<Vec<String>>,
#[serde(flatten)]
pub extra: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QuestionRejectedProps {
pub session_id: String,
pub request_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(flatten)]
pub extra: serde_json::Value,
}
impl Event {
pub fn session_id(&self) -> Option<&str> {
match self {
Self::SessionCreated { properties }
| Self::SessionUpdated { properties }
| Self::SessionDeleted { properties } => Some(&properties.info.id),
Self::SessionIdle { properties } => Some(&properties.session_id),
Self::SessionError { properties } => properties.session_id.as_deref(),
Self::MessageUpdated { properties } => properties.info.session_id.as_deref(),
Self::MessageRemoved { properties } => Some(&properties.session_id),
Self::MessagePartUpdated { properties } | Self::MessagePartDelta { properties } => {
properties.session_id.as_deref()
}
Self::PermissionAsked { properties } => Some(&properties.request.session_id),
Self::PermissionReplied { properties } | Self::PermissionRepliedNext { properties } => {
Some(&properties.session_id)
}
Self::QuestionAsked { properties } => Some(&properties.request.session_id),
Self::QuestionReplied { properties } => Some(&properties.session_id),
Self::QuestionRejected { properties } => Some(&properties.session_id),
_ => None,
}
}
pub fn is_heartbeat(&self) -> bool {
matches!(self, Self::ServerHeartbeat { .. })
}
pub fn is_connected(&self) -> bool {
matches!(self, Self::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",
"slug": "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_todo_updated() {
let json = r#"{"type":"todo.updated","properties":{}}"#;
let event: Event = serde_json::from_str(json).unwrap();
assert!(matches!(event, Event::TodoUpdated { .. }));
}
#[test]
fn test_event_deserialize_question_asked() {
let json = r#"{
"type": "question.asked",
"properties": {
"id": "req-123",
"sessionId": "sess-456",
"questions": [{"question": "Continue?"}]
}
}"#;
let event: Event = serde_json::from_str(json).unwrap();
assert!(matches!(event, Event::QuestionAsked { .. }));
assert_eq!(event.session_id(), Some("sess-456"));
}
#[test]
fn test_event_deserialize_question_replied() {
let json = r#"{
"type": "question.replied",
"properties": {
"sessionId": "sess-456",
"requestId": "req-123",
"answers": [["Yes"]]
}
}"#;
let event: Event = serde_json::from_str(json).unwrap();
if let Event::QuestionReplied { properties } = &event {
assert_eq!(properties.session_id, "sess-456");
assert_eq!(properties.request_id, "req-123");
assert_eq!(properties.answers, vec![vec!["Yes"]]);
} else {
panic!("Expected QuestionReplied");
}
}
#[test]
fn test_event_deserialize_question_rejected() {
let json = r#"{
"type": "question.rejected",
"properties": {
"sessionId": "sess-456",
"requestId": "req-123",
"reason": "User cancelled"
}
}"#;
let event: Event = serde_json::from_str(json).unwrap();
if let Event::QuestionRejected { properties } = &event {
assert_eq!(properties.session_id, "sess-456");
assert_eq!(properties.request_id, "req-123");
assert_eq!(properties.reason, Some("User cancelled".to_string()));
} else {
panic!("Expected QuestionRejected");
}
}
#[test]
fn test_message_part_delta_deserialize() {
let json = r#"{"type": "message.part.delta", "properties": {"sessionId": "ses-1", "messageId": "msg-1", "index": 0}}"#;
let event: Event = serde_json::from_str(json).unwrap();
assert!(matches!(event, Event::MessagePartDelta { .. }));
assert_eq!(event.session_id(), Some("ses-1"));
}
#[test]
fn test_workspace_events_deserialize() {
let ready = r#"{"type": "workspace.ready", "properties": {}}"#;
let failed = r#"{"type": "workspace.failed", "properties": {}}"#;
assert!(matches!(
serde_json::from_str::<Event>(ready).unwrap(),
Event::WorkspaceReady { .. }
));
assert!(matches!(
serde_json::from_str::<Event>(failed).unwrap(),
Event::WorkspaceFailed { .. }
));
}
#[test]
fn test_worktree_events_deserialize() {
let ready = r#"{"type": "worktree.ready", "properties": {}}"#;
let failed = r#"{"type": "worktree.failed", "properties": {}}"#;
assert!(matches!(
serde_json::from_str::<Event>(ready).unwrap(),
Event::WorktreeReady { .. }
));
assert!(matches!(
serde_json::from_str::<Event>(failed).unwrap(),
Event::WorktreeFailed { .. }
));
}
#[test]
fn test_mcp_browser_open_failed_deserialize() {
let json = r#"{"type": "mcp.browser.open.failed", "properties": {}}"#;
let event: Event = serde_json::from_str(json).unwrap();
assert!(matches!(event, Event::McpBrowserOpenFailed { .. }));
}
}