/** * F7 preview webview client (sealed sandbox, INV-21). Receives annotated HTML * from the extension host and swaps it in; runs mermaid over `.mermaid` blocks * (mermaid needs a DOM, so it runs here, not in the host). Bundled by esbuild as * a standalone IIFE → out/media/preview.js, so mermaid never enters the host * bundle. No network, no LLM. */ import mermaid from "mermaid"; // Imported so esbuild emits the sibling out/media/preview.css (the controller // links it into the sealed shell via asWebviewUri). import "./preview.css"; declare function acquireVsCodeApi(): { postMessage(m: unknown): void }; interface RenderMessage { type: "render"; mode: "on" | "off"; html: string; epoch?: string; summary?: { added: number; removed: number; proposals: number }; } const vscodeApi = acquireVsCodeApi(); const body = document.getElementById("cw-body")!; const header = document.getElementById("cw-epoch")!; const summary = document.getElementById("cw-summary")!; const legend = document.getElementById("cw-legend")!; const annotationsEl = document.getElementById("cw-annotations") as HTMLInputElement | null; const pinEl = document.getElementById("cw-pin") as HTMLButtonElement | null; // F10: the annotations on/off toggle. annotationsEl?.addEventListener("change", () => { vscodeApi.postMessage({ type: "setMode", mode: annotationsEl.checked ? "on" : "off" }); }); // F11 (SLICE-1): Pin baseline — post intent; the host pins via the F6 store (INV-35). pinEl?.addEventListener("click", () => { vscodeApi.postMessage({ type: "pinBaseline" }); }); // F10: delegated ✓/✗ accept/reject of pending proposals (routed back to the F4 seam). body.addEventListener("click", (e) => { const btn = (e.target as HTMLElement)?.closest(".cw-actions button"); if (!btn) return; const block = btn.closest(".cw-proposal"); const id = block?.dataset.proposalId; const action = btn.dataset.action; if (id && (action === "accept" || action === "reject")) { vscodeApi.postMessage({ type: action, proposalId: id }); } }); function themeFor(): "dark" | "default" { return document.body.classList.contains("vscode-dark") || document.body.classList.contains("vscode-high-contrast") ? "dark" : "default"; } async function renderMermaid(): Promise { const nodes = Array.from(body.querySelectorAll("pre.mermaid")); if (nodes.length === 0) return; mermaid.initialize({ startOnLoad: false, theme: themeFor(), securityLevel: "strict" }); try { await mermaid.run({ nodes }); } catch { // mermaid.run already marks failed nodes; ensure a visible chip per failure. for (const n of nodes) { if (!n.querySelector("svg")) n.setAttribute("data-cw-error", "true"); } } } window.addEventListener("message", (event: MessageEvent) => { const msg = event.data; if (msg?.type !== "render") return; body.innerHTML = msg.html; const on = msg.mode === "on"; if (annotationsEl) annotationsEl.checked = on; // Off-state is a clean preview: hide the review chrome. header.hidden = !on; summary.hidden = !on; legend.hidden = true; if (on) { header.textContent = `Review since ${msg.epoch ?? ""}`; summary.innerHTML = `+${msg.summary?.added ?? 0} ` + `−${msg.summary?.removed ?? 0} ` + `${msg.summary?.proposals ?? 0} proposal${(msg.summary?.proposals ?? 0) === 1 ? "" : "s"}`; } void renderMermaid(); });