Files
Michael Ramos c2950e709f fix: pre-release QA findings for 0.27.9 (#1405)
Fixes from the 0.27.9 pre-release review. Servers: an unreadable rendered-HTML root falls back to the startup snapshot on both runtimes with a once-per-process warning instead of hanging (Pi) or answering 500 (Bun); the version diff is recomputed against current bytes on reload and carried through the in-app Refresh instead of being dropped, with no history write on a GET. Client: a Refresh action on the compact touch shell; HtmlSurfaceControls renders Refresh independently of the eye; the dead HtmlSurfaceActions removed. Threading: one linear, cycle-safe reply resolution shared by the annotations panel, its sort, and the export (5,000-chain tests), PATCH ingest on both runtimes rejects self-references and cycles, nothing is ever dropped from feedback. WebMCP and viewer hygiene: bounded tombstone and request memories, per-instance minted ids, nudge id caps, waiter cleanup on unmount, a shared retry epoch for diagram blocks. Docs: HTML Refresh documented, the WebMCP design pointer fixed, marketing pages updated.

AI-assisted (Claude) under maintainer direction.
2026-08-27 15:23:28 -07:00

93 lines
3.3 KiB
TypeScript

import { useCallback } from 'react';
import { toast } from 'sonner';
import {
useHtmlRefresh as usePublishedHtmlRefresh,
type HtmlRefreshSnapshot,
} from '@plannotator/ui/hooks/useHtmlRefresh';
import { fetchHtmlDocumentSnapshot, type HtmlVersionDiffFields } from '../sourceDocumentClient';
/** What a refresh hands the app: the bytes plus, for the root document, the
* version-diff fields the server recomputed against them. */
export interface HtmlRefreshedDocument extends HtmlVersionDiffFields {
rawHtml: string;
}
interface UseHtmlRefreshOptions {
enabled: boolean;
activePath: string | null;
onSnapshot: (document: HtmlRefreshedDocument) => void;
/** The ids the remounted viewer could not re-anchor, once per refresh. */
onUnanchored?: (missingIds: string[]) => void;
}
interface UseHtmlRefreshResult {
canRefresh: boolean;
isRefreshing: boolean;
reloadGeneration: number;
refresh: () => Promise<void>;
reportAnnotationRestore: (missingIds: string[]) => void;
}
/**
* Plannotator's binding of the published `useHtmlRefresh`: the snapshot
* comes from `/api/doc` through `fetchHtmlDocumentSnapshot` (which, for the
* session's root document, also carries the recomputed version diff), URL
* sessions (http(s) paths) cannot refresh, and every outcome toasts.
*/
export function useHtmlRefresh({
enabled,
activePath,
onSnapshot,
onUnanchored,
}: UseHtmlRefreshOptions): UseHtmlRefreshResult {
const canRefresh = enabled && !!activePath && !/^https?:\/\//i.test(activePath);
const fetchSnapshot = useCallback(async (path: string | null): Promise<HtmlRefreshSnapshot<HtmlVersionDiffFields>> => {
const result = await fetchHtmlDocumentSnapshot(path ?? '');
if (result.status !== 'ok') return { status: result.status };
const { rawHtml, diffHtml, previousPlan, versionInfo } = result.snapshot;
return { status: 'ok', rawHtml, diffHtml, previousPlan, versionInfo };
}, []);
const handleSnapshot = useCallback((rawHtml: string, snapshot: HtmlVersionDiffFields) => {
onSnapshot({
rawHtml,
diffHtml: snapshot.diffHtml,
previousPlan: snapshot.previousPlan,
versionInfo: snapshot.versionInfo,
});
}, [onSnapshot]);
const handleResult = useCallback((result: 'refreshed' | 'missing' | 'unavailable') => {
if (result === 'missing') {
toast.error('HTML file no longer exists', {
description: 'The current rendered version remains open.',
});
} else if (result === 'unavailable') {
toast.error('Could not refresh HTML', {
description: 'The current rendered version remains open. Try again in a moment.',
});
} else {
toast.success('Refreshed HTML from disk');
}
}, []);
const handleUnanchored = useCallback((missingIds: string[]) => {
onUnanchored?.(missingIds);
if (missingIds.length === 0) return;
toast(`${missingIds.length} annotation${missingIds.length === 1 ? ' no longer matches' : 's no longer match'} the HTML`, {
description: 'Their comments remain available in the annotations panel.',
duration: 5000,
});
}, [onUnanchored]);
return usePublishedHtmlRefresh<HtmlVersionDiffFields>({
enabled: canRefresh,
documentKey: activePath,
fetchSnapshot,
onSnapshot: handleSnapshot,
onUnanchored: handleUnanchored,
onResult: handleResult,
});
}