From 0911cf5db178954d2765290db1f9183f0dc44607 Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Mon, 12 Oct 2026 21:13:04 -0400 Subject: [PATCH] fix(app): Replay activity JavaScript when the final sandbox is recreated --- .../open-generative-ui-renderer.test.tsx | 26 +++++++++++++++++++ .../open-generative-ui/renderer.tsx | 4 +-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/apps/app/src/components/generative-ui/open-generative-ui/__tests__/open-generative-ui-renderer.test.tsx b/apps/app/src/components/generative-ui/open-generative-ui/__tests__/open-generative-ui-renderer.test.tsx index 65cdb2a..d4ac8ed 100644 --- a/apps/app/src/components/generative-ui/open-generative-ui/__tests__/open-generative-ui-renderer.test.tsx +++ b/apps/app/src/components/generative-ui/open-generative-ui/__tests__/open-generative-ui-renderer.test.tsx @@ -647,4 +647,30 @@ describe("OpenGenUIActivityRenderer", () => { }); expect(parsed.success).toBe(true); }); + + it("replays unchanged activity scripts into a recreated final sandbox", async () => { + const jsFunctions = "function greet() { return 'hi'; }"; + const jsExpressions = ["greet();"]; + const base = { htmlComplete: true, generating: false, jsFunctions, jsExpressions }; + const { rerender } = renderRenderer({ ...base, html: ["first"] }); + await flushImport(); await resolveSandboxReady(); + resetMockPromise(); + rerender(rendererElement({ ...base, html: ["replacement"] })); + await flushImport(); await resolveSandboxReady(); + expect(mockCreate).toHaveBeenCalledTimes(2); + expect(mockRun.mock.calls.filter((call) => call[0] === jsFunctions)).toHaveLength(2); + expect(mockRun.mock.calls.filter((call) => call[0] === "greet();")).toHaveLength(2); + }); + + it("replays unchanged activity scripts after CSS changes replace the frame", async () => { + const base = { html: ["same"], htmlComplete: true, generating: false, cssComplete: true, jsFunctions: "function init() {}", jsExpressions: ["init();"] }; + const { rerender } = renderRenderer({ ...base, css: "p{color:red}" }); + await flushImport(); await resolveSandboxReady(); + resetMockPromise(); + rerender(rendererElement({ ...base, css: "p{color:blue}" })); + await flushImport(); await resolveSandboxReady(); + expect(mockCreate).toHaveBeenCalledTimes(2); + expect(mockRun.mock.calls.filter((call) => call[0] === "init();")).toHaveLength(2); + }); + }); diff --git a/apps/app/src/components/generative-ui/open-generative-ui/renderer.tsx b/apps/app/src/components/generative-ui/open-generative-ui/renderer.tsx index 4fb3a40..1389622 100644 --- a/apps/app/src/components/generative-ui/open-generative-ui/renderer.tsx +++ b/apps/app/src/components/generative-ui/open-generative-ui/renderer.tsx @@ -335,7 +335,7 @@ const OpenGenUIActivityRendererInner = React.memo( } else { pendingQueueRef.current.push(content.jsFunctions); } - }, [content.jsFunctions]); + }, [content.jsFunctions, fullHtml, css, localApi]); // Effect 3 — jsExpressions execution (arrival order, sequential await) useEffect(() => { @@ -358,7 +358,7 @@ const OpenGenUIActivityRendererInner = React.memo( } else { pendingQueueRef.current.push(...newExprs); } - }, [content.jsExpressions?.length]); // eslint-disable-line react-hooks/exhaustive-deps + }, [content.jsExpressions?.length, fullHtml, css, localApi]); // eslint-disable-line react-hooks/exhaustive-deps // Effect 4 — destroy any remaining sandbox on unmount useEffect(() => {