Files
backnotprop__plannotator/packages/editor/sourceDocumentClient.ts
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

109 lines
3.7 KiB
TypeScript

import type { SourceSaveCapability } from '@plannotator/shared/source-save';
type EnabledSourceSaveCapability = Extract<SourceSaveCapability, { enabled: true }>;
export type SourceSaveProbeResult =
| { status: 'ok'; sourceSave: EnabledSourceSaveCapability }
| { status: 'missing' }
| { status: 'unavailable' };
export interface HtmlVersionDiffFields {
/** The previous-version page rendered with inline ins/del highlights. */
diffHtml?: string;
previousPlan?: string | null;
versionInfo?: { version: number; totalVersions: number; project: string };
}
interface SourceDocumentResponse extends HtmlVersionDiffFields {
markdown?: string;
rawHtml?: string;
filepath?: string;
sourceSave?: SourceSaveCapability;
renderAs?: 'markdown' | 'html';
}
type SourceDocumentFetchResult =
| { status: 'ok'; data: SourceDocumentResponse }
| { status: 'missing' }
| { status: 'unavailable' };
export interface SourceDocumentSnapshot {
markdown: string;
sourceSave: EnabledSourceSaveCapability;
}
export type SourceDocumentSnapshotResult =
| { status: 'ok'; snapshot: SourceDocumentSnapshot }
| { status: 'missing' }
| { status: 'unavailable' };
/**
* A rendered-HTML document read from /api/doc. The version-diff fields are
* present only when the server served the session's ROOT document (it
* recomputes them against the bytes just read); linked docs carry none.
*/
export interface HtmlDocumentSnapshot extends HtmlVersionDiffFields {
rawHtml: string;
filepath: string;
}
export type HtmlDocumentSnapshotResult =
| { status: 'ok'; snapshot: HtmlDocumentSnapshot }
| { status: 'missing' }
| { status: 'unavailable' };
async function fetchSourceDocument(path: string): Promise<SourceDocumentFetchResult> {
try {
const res = await fetch(`/api/doc?path=${encodeURIComponent(path)}`);
if (res.status === 404) return { status: 'missing' };
if (!res.ok) return { status: 'unavailable' };
return { status: 'ok', data: await res.json() as SourceDocumentResponse };
} catch {
return { status: 'unavailable' };
}
}
export async function probeSourceSave(path: string): Promise<SourceSaveProbeResult> {
const result = await fetchSourceDocument(path);
if (result.status !== 'ok') return { status: result.status };
const { sourceSave } = result.data;
if (sourceSave?.enabled) return { status: 'ok', sourceSave };
if (sourceSave?.enabled === false && sourceSave.reason === 'missing-file') {
return { status: 'missing' };
}
return { status: 'unavailable' };
}
export async function fetchSourceDocumentSnapshot(path: string): Promise<SourceDocumentSnapshotResult> {
const result = await fetchSourceDocument(path);
if (result.status !== 'ok') return { status: result.status };
const { markdown, renderAs, sourceSave } = result.data;
if (sourceSave?.enabled === false && sourceSave.reason === 'missing-file') {
return { status: 'missing' };
}
if (renderAs === 'html' || typeof markdown !== 'string' || !sourceSave?.enabled) return { status: 'unavailable' };
return { status: 'ok', snapshot: { markdown, sourceSave } };
}
export async function fetchHtmlDocumentSnapshot(path: string): Promise<HtmlDocumentSnapshotResult> {
const result = await fetchSourceDocument(path);
if (result.status !== 'ok') return { status: result.status };
const { filepath, rawHtml, renderAs, diffHtml, previousPlan, versionInfo } = result.data;
if (renderAs !== 'html' || typeof rawHtml !== 'string' || typeof filepath !== 'string') {
return { status: 'unavailable' };
}
return {
status: 'ok',
snapshot: {
rawHtml,
filepath,
...(typeof diffHtml === 'string' ? { diffHtml } : {}),
...(previousPlan !== undefined ? { previousPlan } : {}),
...(versionInfo ? { versionInfo } : {}),
},
};
}