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..09d2ee1 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,25 @@ describe("OpenGenUIActivityRenderer", () => { }); expect(parsed.success).toBe(true); }); + + it("waits for complete streamed function source before injection", async () => { + const base = { html: ["
"], htmlComplete: true, generating: true }; + const partial = "function greet() {"; + const complete = "function greet() { return 'hi'; }"; + const { rerender } = renderRenderer({ ...base, jsFunctions: partial, jsFunctionsComplete: false }); + await flushImport(); + await resolveSandboxReady(); + expect(mockRun.mock.calls.some((call) => call[0] === partial)).toBe(false); + rerender(rendererElement({ ...base, jsFunctions: complete, jsFunctionsComplete: true })); + await flushImport(); + expect(mockRun.mock.calls.filter((call) => call[0] === complete)).toHaveLength(1); + }); + + it("does not run incomplete function source when generation stops", async () => { + const partial = "function greet() {"; + renderRenderer({ html: ["Interrupted"], htmlComplete: true, generating: false, jsFunctions: partial, jsFunctionsComplete: false }); + await flushImport(); await resolveSandboxReady(); + expect(mockRun.mock.calls.some((call) => call[0] === partial)).toBe(false); + }); + }); 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..f11d03f 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 @@ -326,7 +326,14 @@ const OpenGenUIActivityRendererInner = React.memo( // Effect 2 — jsFunctions injection (once per final sandbox) useEffect(() => { - if (!content.jsFunctions || jsFunctionsInjectedRef.current) return; + // Partial function source cannot run until the middleware completes it. + // Completed activities without the flag remain compatible. + if ( + !content.jsFunctions || + content.jsFunctionsComplete === false || + (content.jsFunctionsComplete !== true && content.generating !== false) || + jsFunctionsInjectedRef.current + ) return; jsFunctionsInjectedRef.current = true; const sandbox = sandboxRef.current; @@ -335,7 +342,7 @@ const OpenGenUIActivityRendererInner = React.memo( } else { pendingQueueRef.current.push(content.jsFunctions); } - }, [content.jsFunctions]); + }, [content.jsFunctions, content.jsFunctionsComplete, content.generating]); // Effect 3 — jsExpressions execution (arrival order, sequential await) useEffect(() => {