diff --git a/apps/app/src/components/generative-ui/__tests__/export-utils.test.ts b/apps/app/src/components/generative-ui/__tests__/export-utils.test.ts
index 8853eee..196430d 100644
--- a/apps/app/src/components/generative-ui/__tests__/export-utils.test.ts
+++ b/apps/app/src/components/generative-ui/__tests__/export-utils.test.ts
@@ -88,6 +88,33 @@ describe("assembleStandaloneHtmlFromActivity", () => {
expect(doc).toContain('document.title = "<\\/script>";');
});
+ it("keeps js pieces separate when they omit trailing semicolons", async () => {
+ const doc = assembleStandaloneHtmlFromActivity({
+ jsFunctions: "let count = 0",
+ jsExpressions: [
+ "window.exportFirst = count + 1",
+ "(function () { window.exportSecond = 2 })()",
+ "[3].forEach(function (n) { window.exportThird = n })",
+ ],
+ });
+ const script = doc.slice(
+ doc.lastIndexOf("")
+ );
+ const globals = window as unknown as Record;
+ try {
+ expect(() => new Function(script)()).not.toThrow();
+ await Promise.resolve();
+ expect(globals.exportFirst).toBe(1);
+ expect(globals.exportSecond).toBe(2);
+ expect(globals.exportThird).toBe(3);
+ } finally {
+ delete globals.exportFirst;
+ delete globals.exportSecond;
+ delete globals.exportThird;
+ }
+ });
+
it("neutralizes closing style sequences embedded in generated css", () => {
const doc = assembleStandaloneHtmlFromActivity(
{
diff --git a/apps/app/src/components/generative-ui/export-utils.ts b/apps/app/src/components/generative-ui/export-utils.ts
index 106eec4..fd80fac 100644
--- a/apps/app/src/components/generative-ui/export-utils.ts
+++ b/apps/app/src/components/generative-ui/export-utils.ts
@@ -25,6 +25,13 @@ export interface StandaloneActivityContent {
const WEBSANDBOX_STUB = `window.Websandbox = { connection: { remote: { sendPrompt: async () => {}, openLink: async ({ url }) => { if (/^https:/.test(url)) window.open(url, "_blank", "noopener,noreferrer"); } } } };`;
+// The live sandbox runs jsFunctions and each jsExpression as separate scripts,
+// so generated code often omits trailing semicolons. Joined into one script,
+// automatic semicolon insertion would merge the pieces: "let n = 0" followed
+// by "(function () {...})()" parses as a call on 0. An explicit separator keeps
+// each piece a separate statement.
+const STATEMENT_SEPARATOR = "\n;\n";
+
function escapeScriptClose(js: string): string {
return js.replace(/<\/script/gi, "<\\/script");
}
@@ -56,7 +63,7 @@ export function assembleStandaloneHtmlFromActivity(
...(expressions.length > 0
? [
`(async () => {
-${expressions.map(escapeScriptClose).join("\n")}
+${expressions.map(escapeScriptClose).join(STATEMENT_SEPARATOR)}
})();`,
]
: []),
@@ -64,7 +71,7 @@ ${expressions.map(escapeScriptClose).join("\n")}
const generatedScript =
scriptParts.length > 0
? ``
: "";
return `