fix(ui): stabilize task loading and live feeds (#13095)

Coordinate initial conversation reveal, preserve message identity and reading anchors during live updates, and bound transcript reads with recoverable retries. Cover desktop/mobile navigation and rich task loading with actual-route browser tests.

Verified all Linux CI gates, 5,575 local UI tests, eight layout browser scenarios, and recorded native Codex walkthroughs. Greptile: 5/5; all review findings resolved.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip authored and GitHub committed 2026-09-09 10:00:37 -05:00
1 parent 35fdc0c66b
commit cd4c4ed205
32 files changed
+1286 -179

No files matched your search

@@ -1,8 +1,9 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { HeartbeatRunEvent } from "@paperclipai/shared";
import type { TranscriptEntry } from "@/adapters";
import { heartbeatsApi } from "@/api/heartbeats";
import { nativeRunEventsToTranscript } from "./native-run-events";
import { readTranscriptRequest } from "./read-transcript-request";
const EVENT_PAGE_SIZE = 1_000;
const EVENT_POLL_INTERVAL_MS = 2_000;
@@ -36,90 +37,116 @@ export function useNativeRunTranscripts(runs: readonly NativeRunTranscriptSource
);
const [eventsByRun, setEventsByRun] = useState<Map<string, HeartbeatRunEvent[]>>(new Map());
const [errorsByRun, setErrorsByRun] = useState<Map<string, NativeRunTranscriptError>>(new Map());
const [hydratedRunIds, setHydratedRunIds] = useState<ReadonlySet<string>>(new Set());
const [retryGeneration, setRetryGeneration] = useState(0);
const retry = useCallback(() => setRetryGeneration((value) => value + 1), []);
const projectionCacheRef = useRef(new Map<string, { events: HeartbeatRunEvent[]; transcript: TranscriptEntry[] }>());
const cursorByRunRef = useRef(new Map<string, number>());
useEffect(() => {
let cancelled = false;
let timer: number | null = null;
const controller = new AbortController();
const timers = new Set<number>();
const retainedIds = new Set(nativeRuns.map((run) => run.id));
const retainMap = <T,>(previous: Map<string, T>) => {
const next = new Map([...previous].filter(([id]) => retainedIds.has(id)));
return next.size === previous.size ? previous : next;
};
setEventsByRun(retainMap);
setErrorsByRun(retainMap);
setHydratedRunIds((previous) => {
const next = new Set([...previous].filter((id) => retainedIds.has(id)));
return next.size === previous.size ? previous : next;
});
for (const id of cursorByRunRef.current.keys()) {
if (!retainedIds.has(id)) cursorByRunRef.current.delete(id);
}
const refresh = async (runsToRefresh: readonly NativeRunTranscriptSource[]) => {
const updates = new Map<string, HeartbeatRunEvent[]>();
const errors = new Map<string, NativeRunTranscriptError>();
await Promise.all(runsToRefresh.map(async (run) => {
try {
let cursor = cursorByRunRef.current.get(run.id) ?? 0;
const incoming: HeartbeatRunEvent[] = [];
for (;;) {
const page = await heartbeatsApi.events(run.id, cursor, EVENT_PAGE_SIZE);
if (cancelled) return;
const last = page.at(-1);
const nextCursor = last ? Math.max(cursor, last.seq) : cursor;
incoming.push(...page);
if (page.length < EVENT_PAGE_SIZE || nextCursor === cursor) {
cursor = nextCursor;
break;
}
const refreshRun = async (run: NativeRunTranscriptSource) => {
let failed = false;
try {
let cursor = cursorByRunRef.current.get(run.id) ?? 0;
const incoming: HeartbeatRunEvent[] = [];
for (;;) {
const page = await readTranscriptRequest(
(signal) => heartbeatsApi.events(run.id, cursor, EVENT_PAGE_SIZE, { signal }),
controller.signal,
);
if (cancelled) return;
const last = page.at(-1);
const nextCursor = last ? Math.max(cursor, last.seq) : cursor;
incoming.push(...page.filter((event) => event.seq > cursor));
if (page.length < EVENT_PAGE_SIZE || nextCursor === cursor) {
cursor = nextCursor;
break;
}
if (incoming.length > 0) updates.set(run.id, incoming);
cursorByRunRef.current.set(run.id, cursor);
} catch (error) {
// Keep the last durable cursor; the next poll retries this run only.
errors.set(run.id, {
cursor = nextCursor;
}
// Commit this run's cursor with its rows. A slow sibling must neither
// hold its readiness hostage nor stall live polling for this run.
cursorByRunRef.current.set(run.id, cursor);
if (incoming.length > 0) setEventsByRun((previous) => {
const next = new Map(previous);
next.set(run.id, [...(previous.get(run.id) ?? []), ...incoming]);
return next;
});
setErrorsByRun((previous) => {
if (!previous.has(run.id)) return previous;
const next = new Map(previous);
next.delete(run.id);
return next;
});
} catch (error) {
if (cancelled) return;
failed = true;
setErrorsByRun((previous) => {
if (previous.has(run.id)) return previous;
const next = new Map(previous);
next.set(run.id, {
message: error instanceof Error ? error.message : "Native run activity could not be loaded",
failedAt: new Date().toISOString(),
});
}
}));
if (cancelled) return;
const retainedIds = new Set(nativeRuns.map((run) => run.id));
for (const runId of cursorByRunRef.current.keys()) {
if (!retainedIds.has(runId)) cursorByRunRef.current.delete(runId);
return next;
});
}
setEventsByRun((previous) => {
const next = new Map<string, HeartbeatRunEvent[]>();
for (const runId of retainedIds) {
const current = previous.get(runId) ?? [];
const incoming = updates.get(runId) ?? [];
next.set(runId, incoming.length > 0 ? [...current, ...incoming] : current);
}
return next;
});
setErrorsByRun((previous) => {
const next = new Map<string, NativeRunTranscriptError>();
for (const runId of retainedIds) {
const error = errors.get(runId);
if (error) next.set(runId, previous.get(runId) ?? error);
}
return next;
});
if (nativeRuns.some((run) => isLive(run.status)) || errors.size > 0) {
const retryRuns = nativeRuns.filter(
(run) => isLive(run.status) || errors.has(run.id),
);
timer = window.setTimeout(
() => void refresh(retryRuns),
EVENT_POLL_INTERVAL_MS,
);
if (cancelled) return;
setHydratedRunIds((previous) => previous.has(run.id) ? previous : new Set([...previous, run.id]));
if (isLive(run.status) || failed) {
const timer = window.setTimeout(() => {
timers.delete(timer);
void refreshRun(run);
}, EVENT_POLL_INTERVAL_MS);
timers.add(timer);
}
};
void refresh(nativeRuns);
for (const run of nativeRuns) void refreshRun(run);
return () => {
cancelled = true;
if (timer !== null) window.clearTimeout(timer);
controller.abort();
for (const timer of timers) window.clearTimeout(timer);
};
}, [nativeRuns]);
}, [nativeRuns, retryGeneration]);
const transcriptByRun = useMemo(() => {
const transcripts = new Map<string, TranscriptEntry[]>();
for (const run of nativeRuns) {
transcripts.set(run.id, nativeRunEventsToTranscript(eventsByRun.get(run.id) ?? []));
const events = eventsByRun.get(run.id);
if (!events) continue;
let cached = projectionCacheRef.current.get(run.id);
if (!cached || cached.events !== events) {
cached = { events, transcript: nativeRunEventsToTranscript(events) };
projectionCacheRef.current.set(run.id, cached);
}
transcripts.set(run.id, cached.transcript);
}
for (const id of projectionCacheRef.current.keys()) {
if (!transcripts.has(id)) projectionCacheRef.current.delete(id);
}
return transcripts;
}, [eventsByRun, nativeRuns]);
return { transcriptByRun, errorsByRun };
return {
transcriptByRun, errorsByRun, hydratedRunIds, retry,
isInitialHydrating: nativeRuns.some((run) => !hydratedRunIds.has(run.id)),
};
}