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:
parent
2770630f33
commit
d943bddd9e
7 changed files with 227 additions and 58 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ pub async fn serve(
|
|||
.route("/static/app.js", get(serve_app_js))
|
||||
.route("/api/state", get(api_state))
|
||||
.route("/events/stream", get(events_stream))
|
||||
.route("/events/history", get(events_history))
|
||||
.route("/send", post(post_send))
|
||||
.route("/login/start", post(post_login_start))
|
||||
.route("/login/code", post(post_login_code))
|
||||
|
|
@ -206,6 +207,12 @@ async fn post_send(State(state): State<AppState>, Form(form): Form<SendForm>) ->
|
|||
}
|
||||
}
|
||||
|
||||
async fn events_history(
|
||||
State(state): State<AppState>,
|
||||
) -> axum::Json<Vec<crate::events::LiveEvent>> {
|
||||
axum::Json(state.bus.history())
|
||||
}
|
||||
|
||||
async fn events_stream(
|
||||
State(state): State<AppState>,
|
||||
) -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue