import { useEffect, useRef, useState } from "react";
// Which languages get a live sandboxed preview (RenderBlock) instead of a plain
// syntax block (CodeBlock), and how each becomes a document body, lives in
// ./preview/languages.js. A language like `js` is deliberately absent —
// auto-executing bare script isn't this feature's job (see RenderBlock's doc
// comment for the sandboxing model).
import { PREVIEW_LANGS, RENDERABLE_LANGS } from "./preview/languages.js";
// The execution track's display half: a ```nexus-run fence is a run result
// (source + captured output), not a program to execute here. See run-langs.js.
import { RUN_FENCE_LANG, parseRunResult } from "./preview/run-langs.js";
// Parse content into an array of {type, value, lang, streaming} blocks.
// Handles:
// - ```lang\ncode``` (well-formed)
// - ```code``` (no language, no newline — model formatting bug)
// - ```lang\ncode (unclosed — streaming in progress)
function parseBlocks(content) {
const blocks = [];
let i = 0;
while (i < content.length) {
const fenceStart = content.indexOf("```", i);
if (fenceStart === -1) {
if (i < content.length) blocks.push({ type: "text", value: content.slice(i) });
break;
}
if (fenceStart > i) blocks.push({ type: "text", value: content.slice(i, fenceStart) });
let j = fenceStart + 3;
let lang = "";
// Language specifier is valid only when tag-chars are followed by a newline.
// If there's no newline (e.g. ```pythonprint(...)) treat everything as code.
// Hyphens count: `nexus-run` is a tag this file dispatches on, and real
// languages spell themselves that way too (objective-c, c-sharp).
const langMatch = content.slice(j).match(/^([\w-]+)(\r?\n)/);
if (langMatch) {
lang = langMatch[1];
j += langMatch[0].length;
} else if (/^\r?\n/.test(content.slice(j))) {
j += content[j] === "\r" ? 2 : 1;
}
// else: no newline — leave j as-is, lang stays ""
const closeIdx = content.indexOf("```", j);
if (closeIdx === -1) {
blocks.push({ type: "code", lang, value: content.slice(j), streaming: true });
i = content.length;
} else {
blocks.push({ type: "code", lang, value: content.slice(j, closeIdx).replace(/\n$/, ""), streaming: false });
i = closeIdx + 3;
}
}
return blocks;
}
export function Markdown({ content }) {
if (!content) return null;
const blocks = parseBlocks(content);
return (
{blocks.map((block, i) => {
if (block.type !== "code") return ;
const lang = (block.lang || "").toLowerCase();
if (lang === RUN_FENCE_LANG) {
// A half-streamed envelope is not parseable JSON, so the block shows
// as code until the fence closes and then becomes the run panel.
const run = block.streaming ? null : parseRunResult(block.value);
if (run) return ;
}
return RENDERABLE_LANGS.has(lang)
?
: ;
})}
);
}
// A finished run: the source that was executed and what it printed, in one
// block with an Output/Code toggle.
//
// Nothing executes in the browser here, which is the whole difference from
// RenderBlock below. The program already ran on the host (synapse/code_run.py)
// under the user's per-call approval; by the time this renders, the result is
// history. So there is no iframe, no CSP and no sandbox in this component - the
// only untrusted thing present is *text*, and React escapes it.
//
// Output and stderr are shown together rather than on separate tabs: a program
// that printed three lines and then panicked is telling one story, and splitting
// it hides which half the reader needs. Exit code sits in the header because a
// silent non-zero exit is otherwise invisible.
function RunBlock({ run }) {
const [tab, setTab] = useState("output");
const [copied, setCopied] = useState(false);
const copy = () => {
navigator.clipboard.writeText(run.source.trimEnd()).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 1500);
});
};
const failed = run.exitCode !== null && run.exitCode !== 0;
const empty = !run.stdout.trim() && !run.stderr.trim();
return (
{empty && (
(the program printed nothing)
)}
{run.stdout && (
{run.stdout.replace(/\n$/, "")}
)}
{run.stderr && (
{run.stderr.replace(/\n$/, "")}
)}
) : (
{run.source.trimEnd()}
)}
);
}
// Content-Security-Policy for the rendered preview. Together with the iframe's
// `sandbox` attribute below, this is the entire trust boundary for model-
// authored HTML/SVG, so it stays conservative rather than convenient:
// - script-src/style-src 'unsafe-inline' inline ` below is safe unescaped because this module is emitted as an
// external .js asset - it is never inlined into index.html, where the HTML
// parser would end the surrounding script tag early.
//
// postMessage is the one channel an opaque-origin sandboxed frame still has to
// the parent, and this is the entire protocol over it: one message shape,
// outbound only, carrying a content height and an error string. Nothing flows
// the other way. The parent treats both fields as untrusted data - the height
// is clamped and the message is rendered as text, never as markup - because
// they were produced by the same code the sandbox exists to contain.
//
// Without this the frame is silent: a preview whose script throws just renders
// blank, which is why the server-side validator in synapse/tools.py has to
// guess at runtime failures it can't observe.
const _PREVIEW_BOOTSTRAP = ``;
// Substituted with the real line offset once the document is assembled and its
// shell can be measured. Sits on one line so replacing it can't shift any.
const _OFFSET_TOKEN = "__PREVIEW_LINE_OFFSET__";
/**
* Build the sandboxed document for a fence. Returns {doc, error}: a language
* whose source doesn't parse (JSX, today) has no document to show, and the
* caller renders the message instead of a frame.
*
* The shell - charset, CSP, bootstrap - is identical for every language; only
* the body differs, so only that part goes through the registry. Nothing about
* the sandboxing is per-language and shouldn't be: SVG can carry ` +
_PREVIEW_BOOTSTRAP +
"";
// Lines of shell above the user's own code: the document head, plus whatever
// the language puts in the body ahead of it (the Preact build, for JSX).
const offset = (head.match(/\n/g) || []).length + body.userOffset;
return {
doc: (head + body.html + "