operator inbox view on dashboard; agent ui doesn't clobber typing

This commit is contained in:
müde 2026-05-15 17:23:53 +02:00
parent 070b237d03
commit 06ea0cf283
9 changed files with 132 additions and 12 deletions

View file

@ -112,7 +112,7 @@ async fn dispatch(req: &AgentRequest, agent: &str, broker: &Broker) -> AgentResp
},
},
AgentRequest::OperatorMsg { body } => match broker.send(&Message {
from: "operator".to_owned(),
from: hive_sh4re::OPERATOR_RECIPIENT.to_owned(),
to: agent.to_owned(),
body: body.clone(),
}) {

View file

@ -28,6 +28,16 @@ CREATE INDEX IF NOT EXISTS idx_messages_undelivered
/// may drop events past this; we send a `lagged` notice in their stream.
const EVENT_CHANNEL: usize = 256;
/// One row in a `recent_for()` query — the broker's flat view of a
/// message addressed to a given recipient.
#[derive(Debug, Clone, Serialize)]
pub struct InboxRow {
pub id: i64,
pub from: String,
pub body: String,
pub at: i64,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "snake_case", tag = "kind")]
pub enum MessageEvent {
@ -86,6 +96,32 @@ impl Broker {
Ok(())
}
/// Latest `limit` messages addressed to `recipient`, newest-first.
/// Includes delivered + undelivered alike — used for the operator
/// inbox view on the dashboard. Caller decides what to show.
pub fn recent_for(&self, recipient: &str, limit: u64) -> Result<Vec<InboxRow>> {
let conn = self.conn.lock().unwrap();
let limit_i =
i64::try_from(limit.min(i64::MAX as u64)).unwrap_or(i64::MAX);
let mut stmt = conn.prepare(
"SELECT id, sender, body, sent_at
FROM messages
WHERE recipient = ?1
ORDER BY id DESC
LIMIT ?2",
)?;
let rows = stmt.query_map(params![recipient, limit_i], |row| {
Ok(InboxRow {
id: row.get(0)?,
from: row.get(1)?,
body: row.get(2)?,
at: row.get(3)?,
})
})?;
rows.collect::<rusqlite::Result<Vec<_>>>()
.map_err(Into::into)
}
/// Number of undelivered messages addressed to `recipient`. Non-mutating
/// — used by the harness to surface "N unread" in tool-result status
/// lines without popping the queue.

View file

@ -92,6 +92,10 @@ struct StateSnapshot {
containers: Vec<ContainerView>,
transients: Vec<TransientView>,
approvals: Vec<ApprovalView>,
/// Latest messages addressed to `operator` — surfaces agent replies
/// asynchronously so the operator can see them without watching the
/// live panel during a turn.
operator_inbox: Vec<crate::broker::InboxRow>,
}
#[derive(Serialize)]
@ -217,6 +221,12 @@ async fn api_state(
approval_views.push(view);
}
let operator_inbox = state
.coord
.broker
.recent_for(hive_sh4re::OPERATOR_RECIPIENT, 50)
.unwrap_or_default();
axum::Json(StateSnapshot {
hostname,
manager_port: MANAGER_PORT,
@ -224,6 +234,7 @@ async fn api_state(
containers,
transients,
approvals: approval_views,
operator_inbox,
})
}

View file

@ -84,7 +84,7 @@ async fn dispatch(req: &ManagerRequest, coord: &Coordinator) -> ManagerResponse
},
},
ManagerRequest::OperatorMsg { body } => match coord.broker.send(&Message {
from: "operator".to_owned(),
from: hive_sh4re::OPERATOR_RECIPIENT.to_owned(),
to: MANAGER_AGENT.to_owned(),
body: body.clone(),
}) {