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:
iris 2026-08-31 18:11:13 +02:00 committed by mara
commit 9c72a4ae3e
2 changed files with 34 additions and 2 deletions

View file

@ -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/