diff --git a/packages/browser-extension/src/components/SandboxIframe.ts b/packages/browser-extension/src/components/SandboxIframe.ts deleted file mode 100644 index b7e2f097..00000000 --- a/packages/browser-extension/src/components/SandboxIframe.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { LitElement } from "lit"; -import { customElement, property } from "lit/decorators.js"; - -// @ts-ignore - browser global exists in Firefox -declare const browser: any; - -@customElement("sandbox-iframe") -export class SandboxIframe extends LitElement { - @property() content = ""; - private iframe?: HTMLIFrameElement; - - createRenderRoot() { - return this; - } - - override connectedCallback() { - super.connectedCallback(); - window.addEventListener("message", this.handleMessage); - this.createIframe(); - } - - override disconnectedCallback() { - super.disconnectedCallback(); - window.removeEventListener("message", this.handleMessage); - this.iframe?.remove(); - } - - private handleMessage = (e: MessageEvent) => { - if (e.data.type === "sandbox-ready" && e.source === this.iframe?.contentWindow) { - // Sandbox is ready, send content - this.iframe?.contentWindow?.postMessage( - { - type: "loadContent", - content: this.content, - artifactId: "test", - attachments: [], - }, - "*", - ); - } - }; - - private createIframe() { - this.iframe = document.createElement("iframe"); - this.iframe.sandbox.add("allow-scripts"); - this.iframe.sandbox.add("allow-modals"); - this.iframe.style.width = "100%"; - this.iframe.style.height = "100%"; - this.iframe.style.border = "none"; - - const isFirefox = typeof browser !== "undefined" && browser.runtime !== undefined; - if (isFirefox) { - this.iframe.src = browser.runtime.getURL("sandbox.html"); - } else { - this.iframe.src = chrome.runtime.getURL("sandbox.html"); - } - - this.appendChild(this.iframe); - } - - public updateContent(newContent: string) { - this.content = newContent; - // Recreate iframe for clean state - if (this.iframe) { - this.iframe.remove(); - this.iframe = undefined; - } - this.createIframe(); - } -} diff --git a/packages/browser-extension/src/components/SandboxedIframe.ts b/packages/browser-extension/src/components/SandboxedIframe.ts new file mode 100644 index 00000000..344ce421 --- /dev/null +++ b/packages/browser-extension/src/components/SandboxedIframe.ts @@ -0,0 +1,271 @@ +import { LitElement } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import type { Attachment } from "../utils/attachment-utils.js"; + +// @ts-ignore - browser global exists in Firefox +declare const browser: any; + +@customElement("sandbox-iframe") +export class SandboxIframe extends LitElement { + @property() content = ""; + @property() artifactId = ""; + @property({ attribute: false }) attachments: Attachment[] = []; + + private iframe?: HTMLIFrameElement; + private logs: Array<{ type: "log" | "error"; text: string }> = []; + + createRenderRoot() { + return this; + } + + override connectedCallback() { + super.connectedCallback(); + window.addEventListener("message", this.handleMessage); + this.createIframe(); + } + + override disconnectedCallback() { + super.disconnectedCallback(); + window.removeEventListener("message", this.handleMessage); + this.iframe?.remove(); + } + + private handleMessage = (e: MessageEvent) => { + // Handle sandbox-ready message + if (e.data.type === "sandbox-ready" && e.source === this.iframe?.contentWindow) { + // Sandbox is ready, inject our runtime and send content + const enhancedContent = this.injectRuntimeScripts(this.content); + this.iframe?.contentWindow?.postMessage( + { + type: "loadContent", + content: enhancedContent, + artifactId: this.artifactId, + attachments: this.attachments, + }, + "*", + ); + return; + } + + // Only handle messages for this artifact + if (e.data.artifactId !== this.artifactId) return; + + // Handle console messages + if (e.data.type === "console") { + const log = { + type: e.data.method === "error" ? ("error" as const) : ("log" as const), + text: e.data.text, + }; + this.logs.push(log); + this.dispatchEvent( + new CustomEvent("console", { + detail: log, + bubbles: true, + composed: true, + }), + ); + } else if (e.data.type === "execution-complete") { + // Store final logs + this.logs = e.data.logs || []; + this.dispatchEvent( + new CustomEvent("execution-complete", { + detail: { logs: this.logs }, + bubbles: true, + composed: true, + }), + ); + + // Force reflow when iframe content is ready + if (this.iframe) { + this.iframe.style.display = "none"; + this.iframe.offsetHeight; // Force reflow + this.iframe.style.display = ""; + } + } + }; + + private injectRuntimeScripts(htmlContent: string): string { + // Define the runtime function that will be injected + const runtimeFunction = (artifactId: string, attachments: any[]) => { + // @ts-ignore - window extensions + window.__artifactLogs = []; + const originalConsole = { + log: console.log, + error: console.error, + warn: console.warn, + info: console.info, + }; + + ["log", "error", "warn", "info"].forEach((method) => { + // @ts-ignore + console[method] = (...args: any[]) => { + const text = args + .map((arg: any) => { + try { + return typeof arg === "object" ? JSON.stringify(arg) : String(arg); + } catch { + return String(arg); + } + }) + .join(" "); + // @ts-ignore + window.__artifactLogs.push({ type: method === "error" ? "error" : "log", text }); + window.parent.postMessage( + { + type: "console", + method, + text, + artifactId, + }, + "*", + ); + // @ts-ignore + originalConsole[method].apply(console, args); + }; + }); + + window.addEventListener("error", (e: ErrorEvent) => { + const text = e.message + " at line " + e.lineno + ":" + e.colno; + // @ts-ignore + window.__artifactLogs.push({ type: "error", text }); + window.parent.postMessage( + { + type: "console", + method: "error", + text, + artifactId, + }, + "*", + ); + }); + + window.addEventListener("unhandledrejection", (e: PromiseRejectionEvent) => { + const text = "Unhandled promise rejection: " + (e.reason?.message || e.reason || "Unknown error"); + // @ts-ignore + window.__artifactLogs.push({ type: "error", text }); + window.parent.postMessage( + { + type: "console", + method: "error", + text, + artifactId, + }, + "*", + ); + }); + + // Attachment helpers + // @ts-ignore + window.attachments = attachments; + // @ts-ignore + window.listFiles = () => { + // @ts-ignore + return (window.attachments || []).map((a: any) => ({ + id: a.id, + fileName: a.fileName, + mimeType: a.mimeType, + size: a.size, + })); + }; + // @ts-ignore + window.readTextFile = (attachmentId: string) => { + // @ts-ignore + const a = (window.attachments || []).find((x: any) => x.id === attachmentId); + if (!a) throw new Error("Attachment not found: " + attachmentId); + if (a.extractedText) return a.extractedText; + try { + return atob(a.content); + } catch { + throw new Error("Failed to decode text content for: " + attachmentId); + } + }; + // @ts-ignore + window.readBinaryFile = (attachmentId: string) => { + // @ts-ignore + const a = (window.attachments || []).find((x: any) => x.id === attachmentId); + if (!a) throw new Error("Attachment not found: " + attachmentId); + const bin = atob(a.content); + const bytes = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); + return bytes; + }; + + // Send completion after 2 seconds + const sendCompletion = () => { + window.parent.postMessage( + { + type: "execution-complete", + // @ts-ignore + logs: window.__artifactLogs || [], + artifactId, + }, + "*", + ); + }; + + if (document.readyState === "complete" || document.readyState === "interactive") { + setTimeout(sendCompletion, 2000); + } else { + window.addEventListener("load", () => { + setTimeout(sendCompletion, 2000); + }); + } + }; + + // Convert function to string and wrap in IIFE with parameters + const runtimeScript = ` + + `; + + // Inject at start of
or start of document + const headMatch = htmlContent.match(/]*>/i); + if (headMatch) { + const index = headMatch.index! + headMatch[0].length; + return htmlContent.slice(0, index) + runtimeScript + htmlContent.slice(index); + } + + const htmlMatch = htmlContent.match(/]*>/i); + if (htmlMatch) { + const index = htmlMatch.index! + htmlMatch[0].length; + return htmlContent.slice(0, index) + runtimeScript + htmlContent.slice(index); + } + + return runtimeScript + htmlContent; + } + + private createIframe() { + this.iframe = document.createElement("iframe"); + this.iframe.sandbox.add("allow-scripts"); + this.iframe.sandbox.add("allow-modals"); + this.iframe.style.width = "100%"; + this.iframe.style.height = "100%"; + this.iframe.style.border = "none"; + + const isFirefox = typeof browser !== "undefined" && browser.runtime !== undefined; + if (isFirefox) { + this.iframe.src = browser.runtime.getURL("sandbox.html"); + } else { + this.iframe.src = chrome.runtime.getURL("sandbox.html"); + } + + this.appendChild(this.iframe); + } + + public updateContent(newContent: string) { + this.content = newContent; + // Clear logs for new content + this.logs = []; + // Recreate iframe for clean state + if (this.iframe) { + this.iframe.remove(); + this.iframe = undefined; + } + this.createIframe(); + } + + public getLogs(): Array<{ type: "log" | "error"; text: string }> { + return this.logs; + } +} diff --git a/packages/browser-extension/src/sandbox.js b/packages/browser-extension/src/sandbox.js index 763a304b..90ca7888 100644 --- a/packages/browser-extension/src/sandbox.js +++ b/packages/browser-extension/src/sandbox.js @@ -68,7 +68,7 @@ const originalConsole = { // Error handlers window.addEventListener("error", (e) => { - const text = e.message + " at line " + e.lineno + ":" + e.colno; + const text = (e.error?.stack || e.message || String(e)) + " at line " + (e.lineno || "?") + ":" + (e.colno || "?"); window.__artifactLogs.push({ type: "error", text }); window.parent.postMessage( { @@ -79,6 +79,7 @@ window.addEventListener("error", (e) => { }, "*", ); + return false; }); window.addEventListener("unhandledrejection", (e) => { @@ -154,7 +155,7 @@ window.addEventListener("message", (event) => { "});\n\n" + "// Error handlers\n" + "window.addEventListener('error', (e) => {\n" + - " const text = e.message + ' at line ' + e.lineno + ':' + e.colno;\n" + + " const text = (e.error?.stack || e.message || String(e)) + ' at line ' + (e.lineno || '?') + ':' + (e.colno || '?');\n" + " window.__artifactLogs.push({ type: 'error', text });\n" + " window.parent.postMessage({\n" + " type: 'console',\n" + @@ -162,6 +163,7 @@ window.addEventListener("message", (event) => { " text,\n" + " artifactId: window.__currentArtifactId\n" + " }, '*');\n" + + " return false;\n" + "});\n\n" + "window.addEventListener('unhandledrejection', (e) => {\n" + " const text = 'Unhandled promise rejection: ' + (e.reason?.message || e.reason || 'Unknown error');\n" + @@ -173,8 +175,11 @@ window.addEventListener("message", (event) => { " artifactId: window.__currentArtifactId\n" + " }, '*');\n" + "});\n\n" + - "// Send completion when ready\n" + + "// Send completion after 2 seconds to collect all logs and errors\n" + + "let completionSent = false;\n" + "const sendCompletion = function() {\n" + + " if (completionSent) return;\n" + + " completionSent = true;\n" + " window.parent.postMessage({\n" + " type: 'execution-complete',\n" + " logs: window.__artifactLogs || [],\n" + @@ -182,10 +187,10 @@ window.addEventListener("message", (event) => { " }, '*');\n" + "};\n\n" + "if (document.readyState === 'complete' || document.readyState === 'interactive') {\n" + - " setTimeout(sendCompletion, 0);\n" + + " setTimeout(sendCompletion, 2000);\n" + "} else {\n" + - " window.addEventListener('DOMContentLoaded', function() {\n" + - " setTimeout(sendCompletion, 0);\n" + + " window.addEventListener('load', function() {\n" + + " setTimeout(sendCompletion, 2000);\n" + " });\n" + "}\n" + "" + diff --git a/packages/browser-extension/src/sidepanel.ts b/packages/browser-extension/src/sidepanel.ts index a1b40020..e679df5e 100644 --- a/packages/browser-extension/src/sidepanel.ts +++ b/packages/browser-extension/src/sidepanel.ts @@ -5,7 +5,7 @@ import { FileCode2, Settings } from "lucide"; import "@mariozechner/mini-lit/dist/ThemeToggle.js"; import "./ChatPanel.js"; import "./live-reload.js"; -import "./components/SandboxIframe.js"; +import "./components/SandboxedIframe.js"; import type { ChatPanel } from "./ChatPanel.js"; import { ApiKeysDialog } from "./dialogs/ApiKeysDialog.js"; diff --git a/packages/browser-extension/src/tools/artifacts/HtmlArtifact.ts b/packages/browser-extension/src/tools/artifacts/HtmlArtifact.ts index e1370e4e..7cdc0a82 100644 --- a/packages/browser-extension/src/tools/artifacts/HtmlArtifact.ts +++ b/packages/browser-extension/src/tools/artifacts/HtmlArtifact.ts @@ -4,8 +4,10 @@ import { html } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { createRef, type Ref, ref } from "lit/directives/ref.js"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; +import type { SandboxIframe } from "../../components/SandboxedIframe.js"; import type { Attachment } from "../../utils/attachment-utils.js"; import { i18n } from "../../utils/i18n.js"; +import "../../components/SandboxedIframe.js"; import { ArtifactElement } from "./ArtifactElement.js"; @customElement("html-artifact") @@ -15,11 +17,10 @@ export class HtmlArtifact extends ArtifactElement { @property({ attribute: false }) attachments: Attachment[] = []; private _content = ""; - private iframe?: HTMLIFrameElement; private logs: Array<{ type: "log" | "error"; text: string }> = []; // Refs for DOM elements - private iframeContainerRef: Ref