e0118bb835
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
80 lines
3.0 KiB
TypeScript
80 lines
3.0 KiB
TypeScript
/**
|
||
* 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: "changes" | "authorship";
|
||
html: string;
|
||
epoch?: string;
|
||
summary?: { added: number; removed: number; changed: number };
|
||
legend?: { claude: boolean; human: boolean };
|
||
}
|
||
|
||
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 segs = Array.from(document.querySelectorAll<HTMLButtonElement>(".cw-seg"));
|
||
|
||
for (const seg of segs) {
|
||
seg.addEventListener("click", () => {
|
||
vscodeApi.postMessage({ type: "setMode", mode: seg.dataset.mode });
|
||
});
|
||
}
|
||
|
||
function themeFor(): "dark" | "default" {
|
||
return document.body.classList.contains("vscode-dark") ||
|
||
document.body.classList.contains("vscode-high-contrast")
|
||
? "dark"
|
||
: "default";
|
||
}
|
||
|
||
async function renderMermaid(): Promise<void> {
|
||
const nodes = Array.from(body.querySelectorAll<HTMLElement>("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<RenderMessage>) => {
|
||
const msg = event.data;
|
||
if (msg?.type !== "render") return;
|
||
body.innerHTML = msg.html;
|
||
for (const seg of segs) seg.classList.toggle("cw-seg-on", seg.dataset.mode === msg.mode);
|
||
const authorship = msg.mode === "authorship";
|
||
header.hidden = authorship;
|
||
summary.hidden = authorship;
|
||
legend.hidden = !authorship;
|
||
if (authorship) {
|
||
const parts: string[] = [];
|
||
if (msg.legend?.claude) parts.push('<span class="cw-by-claude cw-swatch">Claude</span>');
|
||
if (msg.legend?.human) parts.push('<span class="cw-by-human cw-swatch">You</span>');
|
||
legend.innerHTML = parts.join(" ") || "no attribution yet";
|
||
} else {
|
||
header.textContent = `Track changes since ${msg.epoch ?? ""}`;
|
||
summary.innerHTML =
|
||
`<span class="cw-add">+${(msg.summary?.added ?? 0) + (msg.summary?.changed ?? 0)}</span> ` +
|
||
`<span class="cw-del">−${(msg.summary?.removed ?? 0) + (msg.summary?.changed ?? 0)}</span>`;
|
||
}
|
||
void renderMermaid();
|
||
});
|