Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/frontend/packages/agent/src/hooks/useAgentState.ts
atlas ed271e655f agents: remove the hive-level /effort control too
mara on #5034: "effort shares /models fate". The per-agent web UI no
longer sets effort: the `/effort` slash command, the effort picker,
`POST /api/effort`, `Bus::set_effort`, the `hyperhive-effort` writer,
`EFFORT_LEVELS`/`is_valid_effort` and the ACP effort-list payload are
gone, and `lib/modelEffort.ts` with them. The effort chip is
display-only and shows its source (`effort_source` in `/api/state`);
on ACP it stays hidden while the session offers no effort level.

The read side is unchanged: with no swarm value an agent runs an
existing `hyperhive-effort` file, else its agent.nix effort, else
medium. Existing files are not touched.

Refs #5034
2026-10-11 01:28:44 +02:00

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 pause/resume 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 };
}