Skip to main content

tmai_core/api/
events.rs

1//! Core event system for push-based change notification.
2//!
3//! The event system supports two modes:
4//! - **Bridge mode**: `start_monitoring()` spawns a Poller internally and
5//!   bridges `PollMessage` → `CoreEvent` automatically (for headless/web-only).
6//! - **External mode**: The consumer (TUI) runs its own Poller and calls
7//!   `notify_agents_updated()` / `notify_teams_updated()` to emit events.
8
9use tokio::sync::broadcast;
10
11use super::core::TmaiCore;
12
13/// Events emitted by the core when state changes occur.
14///
15/// Consumers call [`TmaiCore::subscribe()`] to receive these events
16/// via a `broadcast::Receiver`.
17#[derive(Debug, Clone)]
18pub enum CoreEvent {
19    /// The full agent list was refreshed (after a poll cycle)
20    AgentsUpdated,
21
22    /// A single agent changed status
23    AgentStatusChanged {
24        /// Agent target ID
25        target: String,
26        /// Previous status description
27        old_status: String,
28        /// New status description
29        new_status: String,
30    },
31
32    /// A new agent appeared
33    AgentAppeared {
34        /// Agent target ID
35        target: String,
36    },
37
38    /// An agent disappeared
39    AgentDisappeared {
40        /// Agent target ID
41        target: String,
42    },
43
44    /// Team data was refreshed
45    TeamsUpdated,
46
47    /// A team member became idle (waiting for next task)
48    TeammateIdle {
49        /// Agent target ID
50        target: String,
51        /// Team name
52        team_name: String,
53        /// Member name
54        member_name: String,
55    },
56
57    /// A task was completed
58    TaskCompleted {
59        /// Team name
60        team_name: String,
61        /// Task ID
62        task_id: String,
63        /// Task subject
64        task_subject: String,
65    },
66}
67
68impl TmaiCore {
69    /// Subscribe to core events.
70    ///
71    /// Returns a broadcast receiver that will receive [`CoreEvent`]s.
72    /// If the receiver falls behind, older events are dropped (lagged).
73    pub fn subscribe(&self) -> broadcast::Receiver<CoreEvent> {
74        self.event_sender().subscribe()
75    }
76
77    /// Notify subscribers that the agent list was updated.
78    ///
79    /// Called by external consumers (e.g. TUI main loop) after processing
80    /// `PollMessage::AgentsUpdated`. Ignored if no subscribers are listening.
81    pub fn notify_agents_updated(&self) {
82        let _ = self.event_sender().send(CoreEvent::AgentsUpdated);
83    }
84
85    /// Notify subscribers that team data was updated.
86    ///
87    /// Called by external consumers after team scan completes.
88    pub fn notify_teams_updated(&self) {
89        let _ = self.event_sender().send(CoreEvent::TeamsUpdated);
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use crate::api::builder::TmaiCoreBuilder;
97    use crate::config::Settings;
98
99    #[tokio::test]
100    async fn test_subscribe_receives_events() {
101        let core = TmaiCoreBuilder::new(Settings::default()).build();
102        let mut rx = core.subscribe();
103
104        // Send an event via the internal sender
105        let tx = core.event_sender();
106        tx.send(CoreEvent::AgentsUpdated).unwrap();
107
108        let event = rx.recv().await.unwrap();
109        assert!(matches!(event, CoreEvent::AgentsUpdated));
110    }
111
112    #[tokio::test]
113    async fn test_subscribe_multiple_receivers() {
114        let core = TmaiCoreBuilder::new(Settings::default()).build();
115        let mut rx1 = core.subscribe();
116        let mut rx2 = core.subscribe();
117
118        let tx = core.event_sender();
119        tx.send(CoreEvent::TeamsUpdated).unwrap();
120
121        let e1 = rx1.recv().await.unwrap();
122        let e2 = rx2.recv().await.unwrap();
123        assert!(matches!(e1, CoreEvent::TeamsUpdated));
124        assert!(matches!(e2, CoreEvent::TeamsUpdated));
125    }
126
127    #[tokio::test]
128    async fn test_notify_agents_updated() {
129        let core = TmaiCoreBuilder::new(Settings::default()).build();
130        let mut rx = core.subscribe();
131
132        core.notify_agents_updated();
133
134        let event = rx.recv().await.unwrap();
135        assert!(matches!(event, CoreEvent::AgentsUpdated));
136    }
137
138    #[tokio::test]
139    async fn test_notify_teams_updated() {
140        let core = TmaiCoreBuilder::new(Settings::default()).build();
141        let mut rx = core.subscribe();
142
143        core.notify_teams_updated();
144
145        let event = rx.recv().await.unwrap();
146        assert!(matches!(event, CoreEvent::TeamsUpdated));
147    }
148
149    #[test]
150    fn test_notify_no_subscribers() {
151        let core = TmaiCoreBuilder::new(Settings::default()).build();
152        // Should not panic even with no subscribers
153        core.notify_agents_updated();
154        core.notify_teams_updated();
155    }
156}