Merge branch 'main' into claude/webapp-multi-page-refactor-85v1od

# Conflicts:
#	src/handler/api/static/404.html
#	src/handler/api/static/_next/static/QCIYKmybhnvk7okhoCnyi/_buildManifest.js
#	src/handler/api/static/_next/static/QCIYKmybhnvk7okhoCnyi/_ssgManifest.js
#	src/handler/api/static/_next/static/ga21jhjKhYsutf8F3vo_-/_buildManifest.js
#	src/handler/api/static/_next/static/ga21jhjKhYsutf8F3vo_-/_ssgManifest.js
#	src/handler/api/static/_next/static/zI_TsbchBb39diXA-dnfw/_buildManifest.js
#	src/handler/api/static/_next/static/zI_TsbchBb39diXA-dnfw/_ssgManifest.js
#	src/handler/api/static/index.html
#	src/handler/api/static/index.txt
This commit is contained in:
2026-07-22 22:47:27 -04:00
48 changed files with 190 additions and 102 deletions
+20 -4
View File
@@ -216,6 +216,10 @@ export function DashboardProvider({
const [events, setEvents] = useState<AgentEvent[]>([]);
const eventsRef = useRef<AgentEvent[]>([]);
eventsRef.current = events;
/* Bumped on every selectRun: an in-flight loadRun that resolves after the user picked
* a different run must drop its writes, or run A's checkmark/log/events land on run
* B's panel (the 5s tick keeps polls in flight constantly). */
const runGenRef = useRef(0);
const [approvals, setApprovals] = useState<Approval[]>([]);
const [hosts, setHosts] = useState<Host[]>([]);
const [commands, setCommands] = useState<Command[]>([]);
@@ -275,12 +279,15 @@ export function DashboardProvider({
const loadRun = useCallback(async (projectId: string, name: string) => {
const path = `/projects/${encodeURIComponent(projectId)}/agents/${encodeURIComponent(name)}`;
const gen = runGenRef.current;
const stale = () => gen !== runGenRef.current;
try {
const cm = await clientRef.current.api<Checkmark>(`${path}/checkmark`);
if (stale()) return;
setCheckmark(cm);
setCheckmarkMissing(false);
} catch (e) {
if (e instanceof AuthError) return;
if (e instanceof AuthError || stale()) return;
if ((e as ApiError).status === 404) {
setCheckmark(null);
setCheckmarkMissing(true);
@@ -290,9 +297,10 @@ export function DashboardProvider({
const entries = await clientRef.current.api<LogEntry[]>(
`${path}/log?limit=${LOG_LIMIT}&offset=${logOffsetRef.current}`,
);
if (stale()) return;
setLog(entries);
} catch (e) {
swallow(e);
if (!stale()) swallow(e);
}
try {
// Cursor poll: only events newer than what we already hold come back.
@@ -301,9 +309,16 @@ export function DashboardProvider({
const fresh = await clientRef.current.api<AgentEvent[]>(
`${path}/events?after_id=${after}&limit=500`,
);
if (fresh.length) setEvents((prev) => [...prev, ...fresh]);
if (stale() || fresh.length === 0) return;
// Dedup by id at append time: overlapping polls for the same run can both fetch
// from the same cursor; only genuinely-new rows may append.
setEvents((prev) => {
const last = prev.length ? prev[prev.length - 1].id : 0;
const add = fresh.filter((e) => e.id > last);
return add.length ? [...prev, ...add] : prev;
});
} catch (e) {
swallow(e);
if (!stale()) swallow(e);
}
}, []);
@@ -424,6 +439,7 @@ export function DashboardProvider({
const selectRun = useCallback(
(projectId: string, name: string) => {
runGenRef.current += 1; // invalidate any in-flight loadRun for the previous run
setSelectedRun({ projectId, name });
setLogOffset(0);
logOffsetRef.current = 0;