Pure `nix fmt` output from the commit before this one — no hand edits. 203 files: 52 md, 42 tsx, 32 js, 32 css, 21 ts, 13 html, 8 json, 3 mjs. Reproduce with `nix develop -c nix fmt` on the parent commit; the result should be byte-identical to this tree. None of the 13 `.prettierignore` entries appears here — verified by intersecting the changed-file list against the ignore file, with a control proving the intersection finds a match when one exists.
90 lines
3.7 KiB
TypeScript
90 lines
3.7 KiB
TypeScript
// useAgentState — polls `GET /api/state` every 4s and exposes the latest
|
|
// 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";
|
|
|
|
const POLL_MS = 4000;
|
|
const RETRY_MS = 5000;
|
|
|
|
export interface UseAgentStateResult {
|
|
state: AgentState | null;
|
|
error: string | null;
|
|
/** Force an immediate refresh (e.g. right after a model/effort POST). */
|
|
refresh: () => void;
|
|
}
|
|
|
|
export function useAgentState(): UseAgentStateResult {
|
|
const [state, setState] = useState<AgentState | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
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(() => {
|
|
stoppedRef.current = false;
|
|
|
|
async function poll() {
|
|
try {
|
|
const resp = await fetch("api/state");
|
|
if (!resp.ok) throw new Error(`http ${resp.status}`);
|
|
const s = (await resp.json()) as AgentState;
|
|
if (stoppedRef.current) return;
|
|
setState(s);
|
|
setError(null);
|
|
timerRef.current = setTimeout(poll, POLL_MS);
|
|
} catch (err) {
|
|
if (stoppedRef.current) return;
|
|
setError(err instanceof Error ? err.message : String(err));
|
|
timerRef.current = setTimeout(poll, RETRY_MS);
|
|
}
|
|
}
|
|
pollRef.current = poll;
|
|
poll();
|
|
|
|
return () => {
|
|
stoppedRef.current = true;
|
|
if (timerRef.current) clearTimeout(timerRef.current);
|
|
};
|
|
}, []);
|
|
|
|
function refresh() {
|
|
if (timerRef.current) clearTimeout(timerRef.current);
|
|
stoppedRef.current = false;
|
|
pollRef.current();
|
|
}
|
|
|
|
// 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 };
|
|
}
|