hyperhive/hive-ag3nt/src/events.rs
müde ace13cd785 agent ui: terminal-themed live panel; pretty tool calls; collapsed results
- tool_use renders per-tool (Read /path, Bash $ cmd, send → operator: ...)
- tool_result with >120 chars collapses into <details>; short ones inline
- session_init / result / rate_limit dropped from the panel
- thinking content shown inline if present, fallback indicator otherwise
- TurnStart carries unread count → header badge "· 3 unread"
- per-tool [status] line dropped from envelope; lives in wake prompt + UI
- send form moved below the live panel
- live panel themed as a terminal (crust bg, inset shadow, monospace)
2026-05-15 18:20:58 +02:00

69 lines
2.3 KiB
Rust

//! 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<String> },
}
#[derive(Clone)]
pub struct Bus {
tx: Arc<broadcast::Sender<LiveEvent>>,
}
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<LiveEvent> {
self.tx.subscribe()
}
}
impl Default for Bus {
fn default() -> Self {
Self::new()
}
}