perf(client): cut needless polling for local users
- ConversationView: an ended session (SessionDetail, SessionReplay pass `ended`) polls its transcript every 60 s instead of 3 s and stops rescanning sub-agents; new_event, reconnect and show still fetch at once. - Workspace: GET /api/run follows run-moving WS messages (lane run columns, session created/status change, runs_interrupted) with a 30 s fallback, instead of a blind 5 s poll. - StatusBar: session counts re-read only when a session's status/waiting/archived state changes (60 s fallback); costs re-read at most every 30 s while work is happening and every 2 min otherwise. - Dashboard recent activity: 60 s safety poll (was 10 s) plus remote import messages; WS still prepends new events. - New lib/sessionChange.ts matcher with tests.
This commit is contained in:
@@ -85,6 +85,9 @@ import { endRuns } from "../lib/stopSession";
|
||||
import { ContextMenu, MenuItem, useContextMenu } from "../components/ContextMenu";
|
||||
import { confirmDiscard } from "../lib/confirm";
|
||||
import { keepIfSame } from "../lib/keepIfSame";
|
||||
import { laneChangeMatcher, runKeyOf } from "../lib/laneChange";
|
||||
import { sessionChangeMatcher } from "../lib/sessionChange";
|
||||
import { useLiveRefresh } from "../hooks/useLiveRefresh";
|
||||
import { useWorkLanes } from "../hooks/useLanes";
|
||||
import { LaneConsolePane } from "../components/run/LaneConsolePane";
|
||||
import {
|
||||
@@ -1044,9 +1047,10 @@ export function Workspace() {
|
||||
.catch(() => undefined);
|
||||
}, []);
|
||||
|
||||
// Goes through `keepIfSame`: this fires every 5 s below, and a
|
||||
// fresh-but-identical payload would otherwise re-render the lane strip, the
|
||||
// lane card and the whole console area on every tick for nothing.
|
||||
// Goes through `keepIfSame`: this re-reads on run-moving messages and a
|
||||
// fallback poll below, and a fresh-but-identical payload would otherwise
|
||||
// re-render the lane strip, the lane card and the whole console area for
|
||||
// nothing.
|
||||
const refreshList = useCallback(() => {
|
||||
api.run
|
||||
.list()
|
||||
@@ -1061,33 +1065,30 @@ export function Workspace() {
|
||||
window.dispatchEvent(new CustomEvent(LIVE_RUNS_EVENT, { detail: activeRuns.items }));
|
||||
}, [activeRuns]);
|
||||
|
||||
// Background poll so the run list and history reflect external changes
|
||||
// (server-boot reconciliation, sibling tabs, direct DB edits) even when
|
||||
// no WS event fires. Lighter than typical WS gaps; aggressive enough that
|
||||
// status flips appear within seconds without needing a manual refresh.
|
||||
// The run list follows the WebSocket instead of a blind 5 s poll: a pane
|
||||
// starting, ending or learning its session in slot 0 moves the lane's run
|
||||
// columns (`lane_update`); an agent starting in a free tab (slots 1–9)
|
||||
// shows as a new session, one ending as a session status change, and a pane
|
||||
// dying under its agent as `runs_interrupted`. This page's own starts, stops
|
||||
// and a shown terminal's exit call `refreshList` directly
|
||||
// (`onRunsChanged`). What none of those cover — a shell tab opened or closed
|
||||
// from another browser, server-boot reconciliation — is caught by the
|
||||
// 30 s fallback poll, on reconnect, and when the tab is shown or focused.
|
||||
const [laneRunChanged] = useState(() => laneChangeMatcher(runKeyOf));
|
||||
const [sessionRunChanged] = useState(() => sessionChangeMatcher((s) => `${s.status}`));
|
||||
useEffect(() => {
|
||||
refreshList();
|
||||
const tick = setInterval(() => {
|
||||
// Hidden tab: skip; the visibility handler below re-reads on show.
|
||||
if (document.visibilityState === "visible") refreshList();
|
||||
}, 5000);
|
||||
return () => clearInterval(tick);
|
||||
}, [refreshList]);
|
||||
|
||||
// Refresh whenever the tab regains focus / visibility - typical when the
|
||||
// user comes back from running `claude` in a terminal and wants to see the
|
||||
// current state of every run without waiting for the next poll.
|
||||
useLiveRefresh(refreshList, {
|
||||
intervalMs: 30_000,
|
||||
throttleMs: 1500,
|
||||
match: (msg) =>
|
||||
msg.type === "runs_interrupted" || laneRunChanged(msg) || sessionRunChanged(msg),
|
||||
});
|
||||
useEffect(() => {
|
||||
const onFocus = () => refreshList();
|
||||
const onVis = () => {
|
||||
if (document.visibilityState === "visible") refreshList();
|
||||
};
|
||||
window.addEventListener("focus", onFocus);
|
||||
document.addEventListener("visibilitychange", onVis);
|
||||
return () => {
|
||||
window.removeEventListener("focus", onFocus);
|
||||
document.removeEventListener("visibilitychange", onVis);
|
||||
};
|
||||
return () => window.removeEventListener("focus", onFocus);
|
||||
}, [refreshList]);
|
||||
|
||||
const currentLane = selectedLaneId !== null ? lanes.find((l) => l.id === selectedLaneId) : null;
|
||||
|
||||
Reference in New Issue
Block a user