feat(f7): sealed webview asset + esbuild preview bundle (#21)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-06-11 08:45:18 -07:00
parent 6c3b137ebb
commit a3f9688a91
3 changed files with 126 additions and 2 deletions
+19 -2
View File
@@ -30,13 +30,30 @@ const liveTurnOptions = {
logLevel: "info", logLevel: "info",
}; };
/** @type {import('esbuild').BuildOptions} */
const previewOptions = {
entryPoints: ["media/preview.ts"],
outfile: "out/media/preview.js",
bundle: true,
// The webview is a browser context; mermaid is bundled IN (and ONLY in) this
// asset so it never bloats the extension-host bundle (the @cline/sdk size
// discipline). No externals — everything is shipped to the sealed webview.
platform: "browser",
format: "iife",
target: "es2020",
sourcemap: true,
logLevel: "info",
};
if (watch) { if (watch) {
const ctx = await context(options); const ctx = await context(options);
const ctxLive = await context(liveTurnOptions); const ctxLive = await context(liveTurnOptions);
await Promise.all([ctx.watch(), ctxLive.watch()]); const ctxPreview = await context(previewOptions);
await Promise.all([ctx.watch(), ctxLive.watch(), ctxPreview.watch()]);
console.log("esbuild: watching…"); console.log("esbuild: watching…");
} else { } else {
await build(options); await build(options);
await build(liveTurnOptions); await build(liveTurnOptions);
console.log("esbuild: build complete → out/extension.cjs + out/liveTurn.mjs"); await build(previewOptions);
console.log("esbuild: build complete → out/extension.cjs + out/liveTurn.mjs + out/media/preview.js");
} }
+53
View File
@@ -0,0 +1,53 @@
/* F7 track-changes preview — theme-aware via VS Code webview CSS variables. */
body {
font-family: var(--vscode-font-family);
font-size: var(--vscode-font-size);
color: var(--vscode-foreground);
background: var(--vscode-editor-background);
padding: 0 1.2rem 2rem;
line-height: 1.5;
}
#cw-header {
position: sticky;
top: 0;
background: var(--vscode-editor-background);
border-bottom: 1px solid var(--vscode-panel-border);
padding: 0.5rem 0;
font-size: 0.85em;
opacity: 0.85;
display: flex;
gap: 1rem;
}
#cw-summary .cw-add { color: var(--vscode-gitDecoration-addedResourceForeground); }
#cw-summary .cw-del { color: var(--vscode-gitDecoration-deletedResourceForeground); }
.cw-blk { position: relative; }
ins, .cw-added {
background: var(--vscode-diffEditor-insertedTextBackground, rgba(0, 255, 0, 0.15));
text-decoration: none;
}
del, .cw-removed {
background: var(--vscode-diffEditor-removedTextBackground, rgba(255, 0, 0, 0.15));
text-decoration: line-through;
opacity: 0.7;
}
.cw-changed { outline: 2px solid var(--vscode-diffEditor-insertedTextBackground, rgba(0, 255, 0, 0.25)); outline-offset: 2px; }
.cw-badge {
position: absolute;
top: 0;
right: 0;
font-size: 0.7em;
padding: 0 0.4em;
border-radius: 3px;
background: var(--vscode-badge-background);
color: var(--vscode-badge-foreground);
}
.cw-error {
border: 1px solid var(--vscode-inputValidation-errorBorder);
background: var(--vscode-inputValidation-errorBackground);
color: var(--vscode-errorForeground);
padding: 0.3rem 0.6rem;
border-radius: 3px;
font-size: 0.85em;
}
pre.mermaid { text-align: center; background: transparent; }
pre.mermaid[data-cw-error] { color: var(--vscode-errorForeground); }
+54
View File
@@ -0,0 +1,54 @@
/**
* 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";
interface RenderMessage {
type: "render";
html: string;
epoch: string;
summary: { added: number; removed: number; changed: number };
}
const body = document.getElementById("cw-body")!;
const header = document.getElementById("cw-epoch")!;
const summary = document.getElementById("cw-summary")!;
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;
header.textContent = `Track changes since ${msg.epoch}`;
summary.innerHTML =
`<span class="cw-add">+${msg.summary.added + msg.summary.changed}</span> ` +
`<span class="cw-del">${msg.summary.removed + msg.summary.changed}</span>`;
void renderMermaid();
});