1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//! Process-wide runtime-change events for the GUI dashboard.
//!
//! GUI pages read in-memory registries (running agents, non-agent LLM calls,
//! live transcript snapshots) at render time — there is no poll loop. When a
//! mutation touches any of those in-memory sources it broadcasts a
//! [`RuntimeEvent`] here and the dashboard re-renders on delivery. Emit sites
//! are fire-and-forget; the receiver (the GUI) owns a coalescing stream so a
//! burst of mutations settles into a single re-render.
use std::sync::OnceLock;
/// A GUI-relevant in-memory runtime source changed.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RuntimeEvent {
/// Agent/non-agent registry lifecycle, activity instrumentation, or live
/// transcript content changed.
Registries,
/// The voice pipeline status value changed.
#[cfg(target_os = "macos")]
VoiceStatus,
}
/// Process-wide broadcast of runtime-change events.
pub(crate) static RUNTIME_EVENTS: OnceLock<tokio::sync::broadcast::Sender<RuntimeEvent>> =
OnceLock::new();
/// Initialise the runtime-change broadcast before the iced application runs
/// (same convention as [`crate::gui::LOG_BROADCAST`]) so the dashboard's
/// subscription always has a source — an uninitialized sender would end the
/// subscription stream on its first poll. Idempotent; called from `main`
/// via the [`crate::gui`] warm-up façade.
pub(crate) fn init_runtime_event_tx() {
RUNTIME_EVENTS.get_or_init(|| tokio::sync::broadcast::channel(256).0);
}
/// Broadcast a runtime-change event. Best-effort: when the channel is not yet
/// initialised (tests / headless runs) this is a no-op.
pub(crate) fn publish(event: RuntimeEvent) {
if let Some(tx) = RUNTIME_EVENTS.get() {
let _ = tx.send(event);
}
}