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` / `functions_inventions_recursive` /
33    /// `laboratories_executions`; plugins: whatever they declared in
34    /// their manifest's `viewer_routes[i].type`).
35    #[schemars(title = "Inbound")]
36    Inbound {
37        destination: String,
38        sub_type: String,
39        value: serde_json::Value,
40    },
41    /// Host → iframe. One stdout JSONL line from an objectiveai cli
42    /// binary the host spawned for an `invokeCli` this iframe
43    /// started, terminated by a synthetic `{"type":"end"}` line. No
44    /// sub_type — a single invocation produces a single stream of
45    /// lines.
46    #[schemars(title = "CliCommand")]
47    CliCommand {
48        destination: String,
49        value: serde_json::Value,
50    },
51}
52
53impl Event {
54    /// Tauri channel the event fans out on — the repository name of
55    /// the receiving iframe (or `"objectiveai"` for built-ins).
56    pub fn destination(&self) -> &str {
57        match self {
58            Event::Inbound { destination, .. } => destination,
59            Event::CliCommand { destination, .. } => destination,
60        }
61    }
62}
63
64pub type EventReceiver = mpsc::UnboundedReceiver<Event>;
65pub type EventSender = mpsc::UnboundedSender<Event>;
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use serde_json::json;
71
72    #[test]
73    fn inbound_serializes_with_tag_and_sub_type() {
74        let e = Event::Inbound {
75            destination: "objectiveai".to_string(),
76            sub_type: "agent_completions".to_string(),
77            value: json!({"id": "abc"}),
78        };
79        let s = serde_json::to_string(&e).unwrap();
80        let v: serde_json::Value = serde_json::from_str(&s).unwrap();
81        assert_eq!(v["type"], "inbound");
82        assert_eq!(v["destination"], "objectiveai");
83        assert_eq!(v["sub_type"], "agent_completions");
84        assert_eq!(v["value"], json!({"id": "abc"}));
85
86        let back: Event = serde_json::from_str(&s).unwrap();
87        match back {
88            Event::Inbound {
89                destination,
90                sub_type,
91                value,
92            } => {
93                assert_eq!(destination, "objectiveai");
94                assert_eq!(sub_type, "agent_completions");
95                assert_eq!(value, json!({"id": "abc"}));
96            }
97            _ => panic!("expected Inbound"),
98        }
99    }
100
101    #[test]
102    fn cli_command_serializes_with_tag_and_no_sub_type() {
103        let e = Event::CliCommand {
104            destination: "my_plugin".to_string(),
105            value: json!({"type": "notification", "value": {"x": 1}}),
106        };
107        let s = serde_json::to_string(&e).unwrap();
108        let v: serde_json::Value = serde_json::from_str(&s).unwrap();
109        assert_eq!(v["type"], "cli_command");
110        assert_eq!(v["destination"], "my_plugin");
111        assert!(v.get("sub_type").is_none());
112        assert_eq!(v["value"]["type"], "notification");
113    }
114
115    #[test]
116    fn destination_accessor() {
117        let i = Event::Inbound {
118            destination: "d1".to_string(),
119            sub_type: "s".to_string(),
120            value: json!(null),
121        };
122        let c = Event::CliCommand {
123            destination: "d2".to_string(),
124            value: json!(null),
125        };
126        assert_eq!(i.destination(), "d1");
127        assert_eq!(c.destination(), "d2");
128    }
129}