//! Live event stream for the per-agent web UI. The harness emits one //! `LiveEvent` per interesting thing that happens during a turn — wake-up //! (the popped inbox message), every line claude prints on stdout //! (parsed from `--output-format stream-json`), and the turn-end summary. //! The web UI subscribes via SSE and renders rows live. //! //! Channel type is `tokio::sync::broadcast`. New subscribers see only //! future events; the dashboard JS deals with the cold-start case by //! showing "connecting…" until the first event arrives. use std::sync::Arc; use serde::{Deserialize, Serialize}; use tokio::sync::broadcast; const CHANNEL_CAPACITY: usize = 256; /// One row of the agent's live stream. Serialised to JSON for SSE delivery. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "snake_case")] pub enum LiveEvent { /// Harness popped a wake-up message and is about to invoke claude. /// `unread` is the count of *other* messages still in the inbox at /// that moment — surfaced as a badge in the live panel header. TurnStart { from: String, body: String, unread: u64, }, /// One line of claude's `--output-format stream-json` stdout, parsed as /// a generic JSON value (so we don't have to track every claude-code /// event variant). The frontend pretty-prints by `type` field. Stream(serde_json::Value), /// Free-form note from the harness (e.g. "claude exited 0", /// "stream-json parse error: ..."). Useful when stream-json itself /// fails so the UI doesn't just go silent. Note(String), /// Turn finished. `ok=false` means claude exited non-zero or the /// harness hit a transport error. TurnEnd { ok: bool, note: Option }, } #[derive(Clone)] pub struct Bus { tx: Arc>, } impl Bus { #[must_use] pub fn new() -> Self { let (tx, _) = broadcast::channel(CHANNEL_CAPACITY); Self { tx: Arc::new(tx) } } pub fn emit(&self, event: LiveEvent) { // Lagged subscribers drop events — fine; the UI is a tail, not a log. let _ = self.tx.send(event); } pub fn subscribe(&self) -> broadcast::Receiver { self.tx.subscribe() } } impl Default for Bus { fn default() -> Self { Self::new() } }