Compare commits
2 changed files with 15 additions and 49 deletions
|
|
@ -72,17 +72,6 @@ export function Root() {
|
||||||
const [openPanel, setOpenPanel] = useState<OpenPanel>(null);
|
const [openPanel, setOpenPanel] = useState<OpenPanel>(null);
|
||||||
const liveStreamRef = useRef<LiveStreamHandle>(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
|
// Browser tab title — ports app.js's setHeader logic verbatim: prefer
|
||||||
// the human display name + hive_name ("iris // pr1ma") so tabs read
|
// the human display name + hive_name ("iris // pr1ma") so tabs read
|
||||||
// naturally; fall back to the qualified domain label for multi-hive
|
// naturally; fall back to the qualified domain label for multi-hive
|
||||||
|
|
@ -186,7 +175,7 @@ export function Root() {
|
||||||
// idle/thinking reading from before the harness went offline).
|
// idle/thinking reading from before the harness went offline).
|
||||||
const effectiveTurnState = state.status === 'online' ? state.turn_state : 'offline';
|
const effectiveTurnState = state.status === 'online' ? state.turn_state : 'offline';
|
||||||
const turnDef = STATE_LABELS[effectiveTurnState] ?? { glyph: '○', text: 'offline', tone: 'negative' as BadgeTone };
|
const turnDef = STATE_LABELS[effectiveTurnState] ?? { glyph: '○', text: 'offline', tone: 'negative' as BadgeTone };
|
||||||
const stateAge = fmtAge(now - state.turn_state_since * 1000);
|
const stateAge = fmtAge(Date.now() - state.turn_state_since * 1000);
|
||||||
// Consolidated status badge (mara: "why do we have separate alive
|
// Consolidated status badge (mara: "why do we have separate alive
|
||||||
// badge? ... one badge that shows thinking / idle / paused").
|
// badge? ... one badge that shows thinking / idle / paused").
|
||||||
// Not online → the alive-table's own reading (rate-limited/needs-login/
|
// Not online → the alive-table's own reading (rate-limited/needs-login/
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,5 @@
|
||||||
// useAgentState — polls `GET /api/state` every 4s and exposes the latest
|
// useAgentState — polls `GET /api/state` every 4s and exposes the latest
|
||||||
// snapshot + loading/error status. Also resyncs immediately whenever the
|
// snapshot + loading/error status.
|
||||||
// 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 { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
import type { AgentState } from '../types.js';
|
import type { AgentState } from '../types.js';
|
||||||
|
|
||||||
|
|
@ -23,15 +18,6 @@ export function useAgentState(): UseAgentStateResult {
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const stoppedRef = useRef(false);
|
const stoppedRef = useRef(false);
|
||||||
// Holds the mount effect's self-rescheduling `poll` so `refresh()` can
|
|
||||||
// invoke the *same* function instead of a parallel one-off fetch — a
|
|
||||||
// fresh call still runs through `poll`'s own `setTimeout(poll, …)` tail,
|
|
||||||
// so the loop stays alive afterwards instead of dying silently the
|
|
||||||
// first time something calls `refresh()` (see the review discussion on
|
|
||||||
// this file's PR: an earlier version of `refresh()` did its own
|
|
||||||
// one-off fetch with no reschedule, which permanently killed periodic
|
|
||||||
// polling the first time a caller invoked it).
|
|
||||||
const pollRef = useRef<() => void>(() => {});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
stoppedRef.current = false;
|
stoppedRef.current = false;
|
||||||
|
|
@ -51,7 +37,6 @@ export function useAgentState(): UseAgentStateResult {
|
||||||
timerRef.current = setTimeout(poll, RETRY_MS);
|
timerRef.current = setTimeout(poll, RETRY_MS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pollRef.current = poll;
|
|
||||||
poll();
|
poll();
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -62,29 +47,21 @@ export function useAgentState(): UseAgentStateResult {
|
||||||
|
|
||||||
function refresh() {
|
function refresh() {
|
||||||
if (timerRef.current) clearTimeout(timerRef.current);
|
if (timerRef.current) clearTimeout(timerRef.current);
|
||||||
|
// Re-trigger the same poll loop immediately; the effect's closure
|
||||||
|
// owns `poll`, so the simplest safe re-trigger from outside it is a
|
||||||
|
// fresh fetch here rather than reaching back into the effect.
|
||||||
stoppedRef.current = false;
|
stoppedRef.current = false;
|
||||||
pollRef.current();
|
fetch('api/state')
|
||||||
|
.then((resp) => {
|
||||||
|
if (!resp.ok) throw new Error(`http ${resp.status}`);
|
||||||
|
return resp.json();
|
||||||
|
})
|
||||||
|
.then((s: AgentState) => {
|
||||||
|
setState(s);
|
||||||
|
setError(null);
|
||||||
|
})
|
||||||
|
.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. Empty deps: `refresh` only closes over
|
|
||||||
// refs and setState setters, both stable across renders, so the first
|
|
||||||
// render's closure stays correct for the component's whole lifetime —
|
|
||||||
// and, unlike re-subscribing on every render, doesn't re-attach this
|
|
||||||
// listener every second now that `Root`'s own 1s ticker re-renders the
|
|
||||||
// component that calls this hook.
|
|
||||||
useEffect(() => {
|
|
||||||
function onVisible() {
|
|
||||||
if (document.visibilityState === 'visible') refresh();
|
|
||||||
}
|
|
||||||
document.addEventListener('visibilitychange', onVisible);
|
|
||||||
return () => document.removeEventListener('visibilitychange', onVisible);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { state, error, refresh };
|
return { state, error, refresh };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue