sync/race problems + dropped frames on exit problems

This commit is contained in:
lda committed 2026-09-11 05:39:30 +07:00 Verified
1 parent 823bc81d07
commit e45a941706
6 files changed
+187 -34

No files matched your search

+14 -2
View File
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { Inbox, MoreHorizontal, RefreshCw, Search, Zap } from "lucide-react";
import { toast } from "sonner";
@@ -69,8 +69,12 @@ export function DevicesPage({ agents, onAfterWake, onRefresh }: Props) {
const [refreshing, setRefreshing] = useState(false);
const [wakeBusyKey, setWakeBusyKey] = useState("");
const [details, setDetails] = useState<FleetDevice | null>(null);
const loadSeqRef = useRef(0);
async function loadFleet() {
// Latest request wins: filter changes and debounced searches overlap, and
// an older response must never overwrite fresher state.
const seq = ++loadSeqRef.current;
setLoading(true);
try {
const [nextDevices, nextKnownDevices] = await Promise.all([
@@ -84,12 +88,14 @@ export function DevicesPage({ agents, onAfterWake, onRefresh }: Props) {
}),
fetchKnownDevices(),
]);
if (seq !== loadSeqRef.current) return;
setDevices(nextDevices);
setKnownDevices(nextKnownDevices);
} catch (err) {
if (seq !== loadSeqRef.current) return;
toast.error("Failed to load fleet", { description: String(err) });
} finally {
setLoading(false);
if (seq === loadSeqRef.current) setLoading(false);
}
}
@@ -152,7 +158,13 @@ export function DevicesPage({ agents, onAfterWake, onRefresh }: Props) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [presence, known, agentId]);
// The filter effect above already loads on mount; only react to query edits.
const mountedRef = useRef(false);
useEffect(() => {
if (!mountedRef.current) {
mountedRef.current = true;
return;
}
const timer = window.setTimeout(() => void loadFleet(), 220);
return () => window.clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
+12 -3
View File
@@ -105,6 +105,7 @@ export function TerminalPage({
const modifiersRef = useRef<TerminalModifiers>(NO_TERMINAL_MODIFIERS);
const terminalTouchRef = useRef<TerminalTouch | null>(null);
const terminalInputFocusedBeforeAccessoryRef = useRef(false);
const refreshSessionsRef = useRef<() => void>(() => {});
const [sessions, setSessions] = useState<TerminalSession[]>([]);
const [session, setSession] = useState<TerminalSession | null>(null);
const [connection, setConnection] = useState<TerminalConnectionState>("idle");
@@ -348,6 +349,7 @@ export function TerminalPage({
};
if (control.type === "ready") {
setConnection("ready");
refreshSessionsRef.current();
window.requestAnimationFrame(() => {
fitRef.current?.fit();
lastTerminalSizeRef.current = { rows: 0, cols: 0 };
@@ -387,6 +389,8 @@ export function TerminalPage({
if (current === "exited") return current;
return "disconnected";
});
// The agent may have reaped or reported sessions while we were away.
refreshSessionsRef.current();
};
},
[detachTransport, operatorId, sendResize],
@@ -572,7 +576,7 @@ export function TerminalPage({
});
resizeObserver.observe(hostRef.current);
void restoreTerminalSession();
const sessionRefresh = window.setInterval(() => {
const refreshSessions = () => {
void listTerminals()
.then((listed) => {
if (!cancelled) {
@@ -585,15 +589,20 @@ export function TerminalPage({
// The attached terminal transport remains authoritative while a
// background list refresh is temporarily unavailable.
});
}, 5000);
};
refreshSessionsRef.current = refreshSessions;
// Session lifecycle only changes at connect/disconnect moments or when the
// operator returns to the tab; refresh on those instead of polling.
window.addEventListener("focus", refreshSessions);
return () => {
cancelled = true;
input.dispose();
titleChange.dispose();
resizeObserver.disconnect();
window.removeEventListener("focus", refreshSessions);
refreshSessionsRef.current = () => {};
window.cancelAnimationFrame(resizeFrame);
window.clearInterval(sessionRefresh);
detachTransport();
terminal.dispose();
terminalRef.current = null;
@@ -150,10 +150,14 @@ export function TerminalSessionTabs({
<button
type="button"
className="terminal-tab-close"
disabled={locked}
disabled={locked || connection === "connecting"}
aria-label={`Close ${agentLabel} terminal session`}
title={
locked ? "Attached in another browser" : "Close session"
locked
? "Attached in another browser"
: connection === "connecting"
? "Connecting — wait before closing"
: "Close session"
}
onClick={() => onClose(item)}
>