feat(#1187): dynamic terminal scrollback with paginated history

This commit is contained in:
iris 2026-06-03 20:49:30 +02:00 committed by mara
commit 62841a549d
4 changed files with 227 additions and 26 deletions

View file

@ -19,7 +19,7 @@ use tokio::sync::broadcast;
const CHANNEL_CAPACITY: usize = 256;
/// Max `LiveEvent`s the `Bus` returns from `history()` and keeps in
/// sqlite. Older rows are vacuumed on a periodic sweep.
const HISTORY_CAPACITY: usize = 2000;
pub const HISTORY_CAPACITY: usize = 2000;
/// Path to the persisted event db. Overridable via `HYPERHIVE_EVENTS_DB`
/// for dev / tests; otherwise derived from the agent's harness dir.
fn events_db_path() -> PathBuf {
@ -249,20 +249,61 @@ impl EventStore {
}
fn recent(&self, limit: usize) -> rusqlite::Result<Vec<LiveEvent>> {
let (events, _, _) = self.page(None, limit)?;
Ok(events)
}
/// Fetch up to `limit` events with id < `before_id` (or the most recent
/// `limit` events when `before_id` is `None`). Returns
/// `(events_oldest_first, min_row_id, has_more)`.
fn page(
&self,
before_id: Option<i64>,
limit: usize,
) -> rusqlite::Result<(Vec<LiveEvent>, Option<i64>, bool)> {
let limit_i = i64::try_from(limit).unwrap_or(i64::MAX);
let conn = self.conn.lock().unwrap();
let mut stmt = conn.prepare(
"SELECT payload_json FROM events
ORDER BY id DESC
LIMIT ?1",
)?;
let rows = stmt.query_map(params![limit_i], |row| {
let s: String = row.get(0)?;
Ok(serde_json::from_str::<LiveEvent>(&s).ok())
})?;
let mut out: Vec<LiveEvent> = rows.flatten().flatten().collect();
out.reverse();
Ok(out)
// Fetch one extra row so we can tell whether more exist.
let fetch = limit_i.saturating_add(1);
let rows: Vec<(i64, LiveEvent)> = match before_id {
Some(bid) => {
let mut stmt = conn.prepare(
"SELECT id, payload_json FROM events
WHERE id < ?1
ORDER BY id DESC
LIMIT ?2",
)?;
stmt.query_map(params![bid, fetch], |row| {
let id: i64 = row.get(0)?;
let s: String = row.get(1)?;
Ok(serde_json::from_str::<LiveEvent>(&s).ok().map(|e| (id, e)))
})?
.flatten()
.flatten()
.collect()
}
None => {
let mut stmt = conn.prepare(
"SELECT id, payload_json FROM events
ORDER BY id DESC
LIMIT ?1",
)?;
stmt.query_map(params![fetch], |row| {
let id: i64 = row.get(0)?;
let s: String = row.get(1)?;
Ok(serde_json::from_str::<LiveEvent>(&s).ok().map(|e| (id, e)))
})?
.flatten()
.flatten()
.collect()
}
};
let has_more = rows.len() > limit;
let mut rows: Vec<(i64, LiveEvent)> = rows.into_iter().take(limit).collect();
rows.reverse(); // oldest first
let min_id = rows.first().map(|(id, _)| *id);
let events = rows.into_iter().map(|(_, e)| e).collect();
Ok((events, min_id, has_more))
}
}
@ -834,6 +875,23 @@ impl Bus {
};
store.recent(HISTORY_CAPACITY).unwrap_or_default()
}
/// Paginated history: up to `limit` events before `before_id`
/// (or the most recent `limit` when `before_id` is `None`).
/// Returns `(events_oldest_first, min_row_id, has_more)`.
/// `min_row_id` is the cursor for the next page; pass it as
/// `before_id` on the next call.
#[must_use]
pub fn history_page(
&self,
before_id: Option<i64>,
limit: usize,
) -> (Vec<LiveEvent>, Option<i64>, bool) {
let Some(store) = &self.store else {
return (Vec::new(), None, false);
};
store.page(before_id, limit).unwrap_or_default()
}
}
impl Default for Bus {