agent ui: input lives in terminal section, banner shimmer on activity

agent page restructure:
- send form moves into the terminal panel as a prompt-style row beneath
  the live tail (status line stays above so it still reads as a header).
- live panel + prompt share a single bordered 'terminal-wrap' box.
- harness-alive / login-state status lines drop their decorative ascii
  bookends; just a leading dot/glyph remains.
- banner gradient is now a real css gradient with a shimmer animation
  toggled by an .active class. turn_start adds it, turn_end removes it.
  dashboard side mirrors this: each broker sse event nudges a 4s
  shimmer window.
- dashboard container rows drop their static ▓█▓▒░ / ▒░▒░░ glyph
  prefixes; the role chips already disambiguate m1nd vs ag3nt.
- empty-state placeholders drop the ▓ bookends.

terminal pre-fill: hive-ag3nt::events::Bus grows a 500-event ring
buffer; new GET /events/history endpoint returns it. The agent JS
fetches history before opening the SSE stream so opening the page mid-
turn shows the last N events instead of a blank panel. The replay
walks turn_start/turn_end pairs to seed the banner-active state
correctly if a turn was still open.
This commit is contained in:
müde 2026-05-15 18:54:19 +02:00
parent 2770630f33
commit d943bddd9e
7 changed files with 227 additions and 58 deletions

View file

@ -8,12 +8,17 @@
//! future events; the dashboard JS deals with the cold-start case by
//! showing "connecting…" until the first event arrives.
use std::sync::Arc;
use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use serde::{Deserialize, Serialize};
use tokio::sync::broadcast;
const CHANNEL_CAPACITY: usize = 256;
/// Max `LiveEvent`s the `Bus` keeps in its ring buffer. The web UI fetches
/// this on page load to backfill the terminal so the operator sees the
/// last turn(s) without having to wait for the next one.
const HISTORY_CAPACITY: usize = 500;
/// One row of the agent's live stream. Serialised to JSON for SSE delivery.
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -43,16 +48,27 @@ pub enum LiveEvent {
#[derive(Clone)]
pub struct Bus {
tx: Arc<broadcast::Sender<LiveEvent>>,
history: Arc<Mutex<VecDeque<LiveEvent>>>,
}
impl Bus {
#[must_use]
pub fn new() -> Self {
let (tx, _) = broadcast::channel(CHANNEL_CAPACITY);
Self { tx: Arc::new(tx) }
Self {
tx: Arc::new(tx),
history: Arc::new(Mutex::new(VecDeque::with_capacity(HISTORY_CAPACITY))),
}
}
pub fn emit(&self, event: LiveEvent) {
{
let mut h = self.history.lock().unwrap();
if h.len() == HISTORY_CAPACITY {
h.pop_front();
}
h.push_back(event.clone());
}
// Lagged subscribers drop events — fine; the UI is a tail, not a log.
let _ = self.tx.send(event);
}
@ -60,6 +76,13 @@ impl Bus {
pub fn subscribe(&self) -> broadcast::Receiver<LiveEvent> {
self.tx.subscribe()
}
/// Snapshot of the in-memory event ring buffer, oldest first. Drives the
/// terminal pre-fill when the operator opens the agent page.
#[must_use]
pub fn history(&self) -> Vec<LiveEvent> {
self.history.lock().unwrap().iter().cloned().collect()
}
}
impl Default for Bus {