<script lang="ts">
interface Props {
title: string;
html: string;
messages: unknown[];
}
let { title, html, messages }: Props = $props();
let iframeEl: HTMLIFrameElement | undefined = $state();
// Build closing script tag without writing it literally (Svelte parser would choke)
const SCRIPT_CLOSE = "<" + "/script>";
/**
* VS Code API shim injected into <head>.
* Provides acquireVsCodeApi() so extension webviews can call it.
* Messages from the parent arrive via the standard window 'message' event.
*/
const vscodeApiShim = [
"<script>",
"window.acquireVsCodeApi = function acquireVsCodeApi() {",
" let state = null;",
" return {",
" postMessage(message) {",
" window.parent.postMessage({ type: '__wsb_webview_message__', payload: message }, '*');",
" },",
" setState(value) {",
" state = value;",
" return value;",
" },",
" getState() {",
" return state;",
" },",
" };",
"};",
// Forward keybindings to parent so the app-level shortcut handler can process them.
// Only forwards meta/ctrl combos — normal typing stays in the iframe.
"document.addEventListener('keydown', function(e) {",
" if (e.metaKey || e.ctrlKey) {",
" window.parent.postMessage({",
" type: '__wsb_keydown__',",
" key: e.key,",
" code: e.code,",
" metaKey: e.metaKey,",
" ctrlKey: e.ctrlKey,",
" shiftKey: e.shiftKey,",
" altKey: e.altKey",
" }, '*');",
" }",
"}, true);",
SCRIPT_CLOSE,
].join("\n");
let srcdoc = $derived.by(() => {
let doc = html;
if (html.includes("acquireVsCodeApi")) {
if (html.includes("<head>")) {
doc = html.replace("<head>", `<head>${vscodeApiShim}`);
} else {
doc = `${vscodeApiShim}${html}`;
}
}
return doc;
});
/**
* When the iframe loads, post each pending message into it.
* This is how VS Code delivers messages: the parent posts into the
* iframe's contentWindow. The extension's message listener receives
* them via the standard window 'message' event.
*/
function onIframeLoad() {
const win = iframeEl?.contentWindow;
if (!win || messages.length === 0) return;
for (const message of messages) {
// Round-trip through JSON to ensure the data is plain and cloneable.
// Messages originate from serde_json::Value and may carry non-cloneable artifacts.
win.postMessage(JSON.parse(JSON.stringify(message)), "*");
}
}
</script>
<div class="webview-panel">
<iframe
bind:this={iframeEl}
class="webview-frame"
title={title}
sandbox="allow-scripts allow-same-origin"
srcdoc={srcdoc}
onload={onIframeLoad}
></iframe>
</div>
<style>
.webview-panel {
width: 100%;
height: 100%;
min-height: 0;
background: var(--vscode-tab-activeBackground);
}
.webview-frame {
width: 100%;
height: 100%;
border: 0;
background: white;
}
</style>