From e8bdf7b5f915b5c95288bc1e8d9e28898217fe29 Mon Sep 17 00:00:00 2001 From: jerelvelarde Date: Wed, 25 Mar 2026 07:32:20 -0700 Subject: [PATCH 1/3] fix: revert save-template-overlay to synchronous ref pattern The async useState+useEffect refactor caused matchedTemplate to change after first render, inserting a badge div before the iframe and shifting its child index. React reconciles by position, so this remounted the iframe and destroyed rendered 3D/canvas content. Restores the original synchronous ref with eslint-disable comments. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../generative-ui/save-template-overlay.tsx | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/apps/app/src/components/generative-ui/save-template-overlay.tsx b/apps/app/src/components/generative-ui/save-template-overlay.tsx index 2606ec5..d1fbb15 100644 --- a/apps/app/src/components/generative-ui/save-template-overlay.tsx +++ b/apps/app/src/components/generative-ui/save-template-overlay.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useCallback, useEffect, useMemo, type ReactNode } from "react"; +import { useState, useCallback, useMemo, useRef, type ReactNode } from "react"; import { useAgent } from "@copilotkit/react-core/v2"; import { SEED_TEMPLATES } from "@/components/template-library/seed-templates"; @@ -35,29 +35,28 @@ export function SaveTemplateOverlay({ const [saveState, setSaveState] = useState("idle"); const [templateName, setTemplateName] = useState(""); - // Capture pending_template once — it may be cleared by the agent later. - // Syncs external agent state into local state (legitimate effect-based setState). + // Capture pending_template at mount time — it may be cleared by the agent later. + // Uses ref (not state) to avoid an async re-render that would shift sibling positions + // and cause React to remount the iframe, losing rendered 3D/canvas content. const pending = agent.state?.pending_template as { id: string; name: string } | null | undefined; - const [capturedSource, setCapturedSource] = useState<{ id: string; name: string } | null>(null); - useEffect(() => { - if (pending?.id) { - // eslint-disable-next-line react-hooks/set-state-in-effect -- one-time capture of external agent state - setCapturedSource((prev) => prev ?? pending); - } - }, [pending]); + const sourceRef = useRef<{ id: string; name: string } | null>(null); + // eslint-disable-next-line react-hooks/refs -- one-time ref init during render (React-endorsed pattern) + if (pending?.id && !sourceRef.current) { + sourceRef.current = pending; // eslint-disable-line react-hooks/refs + } // Check if this content matches an existing template: // 1. Exact HTML match (seed templates rendered as-is) // 2. Source template captured from pending_template (applied templates with modified data) const matchedTemplate = useMemo(() => { // First check source template from apply flow - if (capturedSource) { + if (sourceRef.current) { // eslint-disable-line react-hooks/refs const allTemplates = [ ...SEED_TEMPLATES, ...((agent.state?.templates as { id: string; name: string }[]) || []), ]; - const found = allTemplates.find((t) => t.id === capturedSource.id); - if (found) return found; + const source = allTemplates.find((t) => t.id === sourceRef.current!.id); // eslint-disable-line react-hooks/refs + if (source) return source; } // Then check exact HTML match if (!html) return null; @@ -68,7 +67,7 @@ export function SaveTemplateOverlay({ ...((agent.state?.templates as { id: string; name: string; html: string }[]) || []), ]; return allTemplates.find((t) => t.html && normalise(t.html) === norm) ?? null; - }, [html, agent.state?.templates, capturedSource]); + }, [html, agent.state?.templates]); const handleSave = useCallback(() => { const name = templateName.trim() || title || "Untitled Template"; From 2c37a836f61fd3b74f2af4eb078a6dc088079f12 Mon Sep 17 00:00:00 2001 From: jerelvelarde Date: Wed, 25 Mar 2026 08:22:56 -0700 Subject: [PATCH 2/3] fix: resolve bare ES module specifiers in widget iframe for Three.js Add import map to iframe shell mapping common libraries (three, gsap, d3, chart.js) to esm.sh CDN URLs so bare specifiers like `import "three"` resolve correctly. Auto-detect ES module syntax in dynamically injected scripts and promote them to type="module" when import/export statements are present. Update skill instructions to prefer the ES module pattern for Three.js. --- .../skills/advanced-visualization/SKILL.md | 10 ++++++- .../generative-ui/widget-renderer.tsx | 28 ++++++++++++++++--- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/apps/agent/skills/advanced-visualization/SKILL.md b/apps/agent/skills/advanced-visualization/SKILL.md index f40735e..3ee90df 100644 --- a/apps/agent/skills/advanced-visualization/SKILL.md +++ b/apps/agent/skills/advanced-visualization/SKILL.md @@ -668,7 +668,15 @@ Only these CDN origins work (CSP-enforced): ``` -**Three.js** (3D graphics): +**Three.js** (3D graphics) — use ES module import (import map resolves bare specifiers): +```html + +``` +Alternative UMD (global `THREE` variable): ```html ``` diff --git a/apps/app/src/components/generative-ui/widget-renderer.tsx b/apps/app/src/components/generative-ui/widget-renderer.tsx index 6a64a1a..828f5b7 100644 --- a/apps/app/src/components/generative-ui/widget-renderer.tsx +++ b/apps/app/src/components/generative-ui/widget-renderer.tsx @@ -428,7 +428,13 @@ window.addEventListener('message', function(e) { content.setAttribute('data-exec-' + hash, '1'); try { var newScript = document.createElement('script'); - if (scriptInfo.type) newScript.type = scriptInfo.type; + // Auto-detect ES module syntax: if the script contains import/export + // statements but lacks type="module", promote it so the import map applies. + var effectiveType = scriptInfo.type; + if (!effectiveType && scriptInfo.text && /\\b(import\\s|export\\s|import\\()/.test(scriptInfo.text)) { + effectiveType = 'module'; + } + if (effectiveType) newScript.type = effectiveType; if (scriptInfo.src) { newScript.src = scriptInfo.src; newScript.onload = function() { runScripts(scripts, idx + 1); }; @@ -471,6 +477,20 @@ function assembleShell(initialHtml: string = ""): string { + setLoaded(true)} style={{ From d4f4ea60b7668c200cfb3452138d7a1485d5a943 Mon Sep 17 00:00:00 2001 From: jerelvelarde Date: Wed, 25 Mar 2026 11:52:22 -0700 Subject: [PATCH 3/3] chore: add safety comment for allow-same-origin sandbox flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit References issue #3 — allow-same-origin is required for import maps in srcdoc iframes but weakens the sandbox. Document the tradeoff. --- apps/app/src/components/generative-ui/widget-renderer.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/app/src/components/generative-ui/widget-renderer.tsx b/apps/app/src/components/generative-ui/widget-renderer.tsx index 828f5b7..c8bd429 100644 --- a/apps/app/src/components/generative-ui/widget-renderer.tsx +++ b/apps/app/src/components/generative-ui/widget-renderer.tsx @@ -700,6 +700,9 @@ export function WidgetRenderer({ title, description, html }: WidgetRendererProps content streamed via postMessage for progressive rendering. */}