wave 1 (platform stream): background-observability accuracy + owner-driven follow-ups

- observability: liveness-gated stale-job recovery, extraction run registry on globalThis with
  explicit 'embedding' phase, /api/jobs force-dynamic, client refresh nudges at every seam
- headless: providerSafePng canonicalizer (multi-IDAT Chrome PNGs → single-IDAT) fixing qwen 400s
- composer: Enter routes through pure enterAction — accepts a pending plan verdict (owner decision)
- markdown chrome: session-scoped mermaid overlay + md-blocks component set + pan-zoom math,
  corner-copy (clipboard grant in electron main), math/table/code block fixes, heading weight 900

543/543 tests; typecheck/lint/build clean
This commit is contained in:
amosblomqvist
2026-09-19 13:16:34 +03:00
parent cbecb8df03
commit 487f5d4cea
27 changed files with 1754 additions and 111 deletions
+21 -2
View File
@@ -75,6 +75,15 @@ const ActivityContext = createContext<{ feed: ActivityFeed; refresh: () => void
* Polls /api/activity for the whole app shell. Adaptive interval: fast while
* anything runs, slow when idle, ~4× slower on hidden tabs. A failed fetch
* keeps the last feed (the dock never flickers to empty on a blip).
*
* ACCURACY (2026-09-19): polling alone can only be as fresh as its interval,
* so the start sites that the client can know about nudge refresh()
* directly (chat.tsx at turn start/end and dispatch; the extract/import
* buttons), and THIS provider adds the last client-known event: visibility —
* a tab becoming visible refetches at once, so hidden-tab staleness (×4)
* ends the moment the learner looks. The poll stays the safety net for
* everything with no client-visible cause (other tabs, wake turns,
* recovery).
*/
export function ActivityProvider({ children }: { children: React.ReactNode }) {
const [feed, setFeed] = useState<ActivityFeed>(EMPTY_FEED);
@@ -91,7 +100,7 @@ export function ActivityProvider({ children }: { children: React.ReactNode }) {
if (res.ok) {
const data = (await res.json()) as Partial<ActivityFeed>;
if (alive)
setFeed({ jobs: data.jobs ?? [], extractions: data.extractions ?? [] });
setFeed({ jobs: data.jobs ?? [], extractions: data.extractions ?? [] });
}
} catch {
// transport blip: keep the last feed, try again
@@ -107,6 +116,16 @@ export function ActivityProvider({ children }: { children: React.ReactNode }) {
};
}, [hasRunning, nonce]);
// The visibility nudge: returning to the tab refetches immediately (and
// the re-run effect below resets the poll timer — no double polling).
useEffect(() => {
const onVisibility = () => {
if (document.visibilityState === "visible") setNonce((n) => n + 1);
};
document.addEventListener("visibilitychange", onVisibility);
return () => document.removeEventListener("visibilitychange", onVisibility);
}, []);
return <ActivityContext.Provider value={{ feed, refresh }}>{children}</ActivityContext.Provider>;
}
@@ -128,7 +147,7 @@ export interface ActivityBlob {
process: "job" | "extraction";
/** Sub-agent kind for jobs (extraction blobs have none). */
jobKind?: JobKind;
/** Extraction phase (extracting/auditing/merging); jobs carry none. */
/** Extraction phase (extracting/auditing/merging/embedding); jobs carry none. */
phase?: string;
title: string;
chatId: string;