feat(f7): sealed webview asset + esbuild preview bundle (#21)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+19
-2
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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); }
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user