agent web UI: keep the turn-state badge live instead of freezing until reload
The status badge's elapsed-time text ('thinking Xm Ys') was computed
from Date.now() inside the render function, so it only ever advanced
when the component actually re-rendered. Two gaps let it go stale:
- useAgentState's poll loop is a chained setTimeout, which browsers
throttle (or suspend outright) once the tab is backgrounded, so
polling could stall for a long time with no way back to a live
reading short of a full page reload.
- even under healthy polling, the age text only advanced once every
~4s (the poll interval) instead of counting up smoothly.
Fix: resync immediately on visibilitychange (so returning to a
backgrounded tab doesn't leave a stale reading), and drive the age
text off its own 1s interval independent of the poll cadence.
This commit is contained in:
parent
6e6bf62437
commit
9c72a4ae3e
2 changed files with 34 additions and 2 deletions
|
|
@ -72,6 +72,17 @@ export function Root() {
|
|||
const [openPanel, setOpenPanel] = useState<OpenPanel>(null);
|
||||
const liveStreamRef = useRef<LiveStreamHandle>(null);
|
||||
|
||||
// Ticks the status badge's "thinking Xm Ys" age once a second,
|
||||
// independent of the ~4s `/api/state` poll cadence — without its own
|
||||
// clock the age text only advances when a poll happens to land, so it
|
||||
// visibly stair-steps instead of counting up, and freezes outright for
|
||||
// however long a poll is delayed (e.g. a backgrounded tab).
|
||||
const [now, setNow] = useState(() => Date.now());
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setNow(Date.now()), 1000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
// Browser tab title — ports app.js's setHeader logic verbatim: prefer
|
||||
// the human display name + hive_name ("iris // pr1ma") so tabs read
|
||||
// naturally; fall back to the qualified domain label for multi-hive
|
||||
|
|
@ -175,7 +186,7 @@ export function Root() {
|
|||
// idle/thinking reading from before the harness went offline).
|
||||
const effectiveTurnState = state.status === 'online' ? state.turn_state : 'offline';
|
||||
const turnDef = STATE_LABELS[effectiveTurnState] ?? { glyph: '○', text: 'offline', tone: 'negative' as BadgeTone };
|
||||
const stateAge = fmtAge(Date.now() - state.turn_state_since * 1000);
|
||||
const stateAge = fmtAge(now - state.turn_state_since * 1000);
|
||||
// Consolidated status badge (mara: "why do we have separate alive
|
||||
// badge? ... one badge that shows thinking / idle / paused").
|
||||
// Not online → the alive-table's own reading (rate-limited/needs-login/
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
// useAgentState — polls `GET /api/state` every 4s and exposes the latest
|
||||
// snapshot + loading/error status.
|
||||
// snapshot + loading/error status. Also resyncs immediately whenever the
|
||||
// tab regains visibility: a backgrounded tab's `setTimeout` chain gets
|
||||
// throttled/frozen by the browser, so the last snapshot — and anything
|
||||
// the UI derives from it, like the turn-state badge's elapsed-time text —
|
||||
// goes stale until something proactively refetches; without this the
|
||||
// only way back to a live reading was a full page reload.
|
||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||
import type { AgentState } from '../types.js';
|
||||
|
||||
|
|
@ -63,5 +68,21 @@ export function useAgentState(): UseAgentStateResult {
|
|||
.catch((err) => setError(err instanceof Error ? err.message : String(err)));
|
||||
}
|
||||
|
||||
// A backgrounded/hidden tab has its `setTimeout` chain throttled by the
|
||||
// browser (Chrome et al throttle nested timers heavily once hidden, and
|
||||
// may suspend the page outright) — the `poll()` loop above effectively
|
||||
// stalls, and everything derived from `state` (not just the fetch
|
||||
// itself) goes stale until it happens to fire again. Force a resync the
|
||||
// moment the tab becomes visible so switching back doesn't leave a
|
||||
// reading that's minutes old. `refresh` closes over stable refs/setters
|
||||
// only, so a fresh instance each render is safe to use here.
|
||||
useEffect(() => {
|
||||
function onVisible() {
|
||||
if (document.visibilityState === 'visible') refresh();
|
||||
}
|
||||
document.addEventListener('visibilitychange', onVisible);
|
||||
return () => document.removeEventListener('visibilitychange', onVisible);
|
||||
});
|
||||
|
||||
return { state, error, refresh };
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue