Skip to main content

objectiveai_sdk/viewer/
events.rs

1//! Event bus. Built-in axum routes, dynamic plugin routes, and the
2//! cli_command stream all fan into the same enum; the viewer's
3//! `serve()` emits each variant as-is under the `destination` Tauri
4//! channel name.
5//!
6//! Channel-name namespacing: `"objectiveai"` is reserved as the
7//! built-in destination; plugin repositories named "objectiveai"
8//! are refused at install time (see
9//! `filesystem::plugins::InstallError::ReservedRepositoryName`), so
10//! a plugin can't shadow built-in events.
11
12use schemars::JsonSchema;
13use serde::{Deserialize, Serialize};
14use tokio::sync::mpsc;
15
16/// Every event the viewer emits to the JS side. Serde-tagged on
17/// `type` so the JS bridge can pattern-match and decide how to
18/// repackage each variant for the destination iframe.
19///
20/// `destination` is `"objectiveai"` for built-in events, or the
21/// plugin's repository name otherwise. For `CliCommand` it's the
22/// repository name of whichever iframe invoked the CLI — the bridge
23/// derives it from `MessageEvent.source`, the plugin author never
24/// sets it.
25#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
26#[serde(tag = "type", rename_all = "snake_case")]
27#[schemars(rename = "viewer.Event")]
28pub enum Event {
29    /// Host → iframe. Carries data into the plugin (the existing
30    /// path). `sub_type` is the snake_case discriminator the plugin
31    /// listens on (built-ins: `agent_completions` /
32    /// `functions_executions`; plugins: whatever they declared in
33    /// their manifest's `viewer_routes[i].type`).
34    #[schemars(title = "Inbound")]
35    Inbound {
36        destination: String,
37        sub_type: String,
38        value: serde_json::Value,
39    },
40    /// Host → iframe. One stdout JSONL line from an objectiveai cli
41    /// binary the host spawned for an `invokeCli` this iframe
42    /// started, terminated by a synthetic `{"type":"end"}` line. No
43    /// sub_type — a single invocation produces a single stream of
44    /// lines.
45    #[schemars(title = "CliCommand")]
46    CliCommand {
47        destination: String,
48        value: serde_json::Value,
49    },
50}
51
52impl Event {
53    /// Tauri channel the event fans out on — the repository name of
54    /// the receiving iframe (or `"objectiveai"` for built-ins).
55    pub fn destination(&self) -> &str {
56        match self {
57            Event::Inbound { destination, .. } => destination,
58            Event::CliCommand { destination, .. } => destination,
59        }
60    }
61}
62
63pub type EventReceiver = mpsc::UnboundedReceiver<Event>;
64pub type EventSender = mpsc::UnboundedSender<Event>;
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69    use serde_json::json;
70
71    #[test]
72    fn inbound_serializes_with_tag_and_sub_type() {
73        let e = Event::Inbound {
74            destination: "objectiveai".to_string(),
75            sub_type: "agent_completions".to_string(),
76            value: json!({"id": "abc"}),
77        };
78        let s = serde_json::to_string(&e).unwrap();
79        let v: serde_json::Value = serde_json::from_str(&s).unwrap();
80        assert_eq!(v["type"], "inbound");
81        assert_eq!(v["destination"], "objectiveai");
82        assert_eq!(v["sub_type"], "agent_completions");
83        assert_eq!(v["value"], json!({"id": "abc"}));
84
85        let back: Event = serde_json::from_str(&s).unwrap();
86        match back {
87            Event::Inbound {
88                destination,
89                sub_type,
90                value,
91            } => {
92                assert_eq!(destination, "objectiveai");
93                assert_eq!(sub_type, "agent_completions");
94                assert_eq!(value, json!({"id": "abc"}));
95            }
96            _ => panic!("expected Inbound"),
97        }
98    }
99
100    #[test]
101    fn cli_command_serializes_with_tag_and_no_sub_type() {
102        let e = Event::CliCommand {
103            destination: "my_plugin".to_string(),
104            value: json!({"type": "notification", "value": {"x": 1}}),
105        };
106        let s = serde_json::to_string(&e).unwrap();
107        let v: serde_json::Value = serde_json::from_str(&s).unwrap();
108        assert_eq!(v["type"], "cli_command");
109        assert_eq!(v["destination"], "my_plugin");
110        assert!(v.get("sub_type").is_none());
111        assert_eq!(v["value"]["type"], "notification");
112    }
113
114    #[test]
115    fn destination_accessor() {
116        let i = Event::Inbound {
117            destination: "d1".to_string(),
118            sub_type: "s".to_string(),
119            value: json!(null),
120        };
121        let c = Event::CliCommand {
122            destination: "d2".to_string(),
123            value: json!(null),
124        };
125        assert_eq!(i.destination(), "d1");
126        assert_eq!(c.destination(), "d2");
127    }
128}