diff --git a/packages/web-ui/src/ChatPanel.ts b/packages/web-ui/src/ChatPanel.ts index 7bc78392..1ef4886b 100644 --- a/packages/web-ui/src/ChatPanel.ts +++ b/packages/web-ui/src/ChatPanel.ts @@ -221,14 +221,7 @@ export class ChatPanel extends LitElement { ${Badge(html` ${i18n("Artifacts")} - ${ - this.artifactCount > 1 - ? html`${this.artifactCount}` - : "" - } + ${this.artifactCount} `)} diff --git a/packages/web-ui/src/components/SandboxedIframe.ts b/packages/web-ui/src/components/SandboxedIframe.ts index 8a020404..bb913e7e 100644 --- a/packages/web-ui/src/components/SandboxedIframe.ts +++ b/packages/web-ui/src/components/SandboxedIframe.ts @@ -163,42 +163,37 @@ export class SandboxIframe extends LitElement { throw new Error("Execution aborted"); } - providers = [new ConsoleRuntimeProvider(), ...providers]; + const consoleProvider = new ConsoleRuntimeProvider(); + providers = [consoleProvider, ...providers]; RUNTIME_MESSAGE_ROUTER.registerSandbox(sandboxId, providers, consumers); - const logs: Array<{ type: string; text: string }> = []; const files: SandboxFile[] = []; let completed = false; return new Promise((resolve, reject) => { // 4. Create execution consumer for lifecycle messages const executionConsumer: MessageConsumer = { - async handleMessage(message: any): Promise { - if (message.type === "console") { - logs.push({ - type: message.method === "error" ? "error" : "log", - text: message.text, - }); - return true; - } else if (message.type === "file-returned") { + async handleMessage(message: any): Promise { + if (message.type === "file-returned") { files.push({ fileName: message.fileName, content: message.content, mimeType: message.mimeType, }); - return true; } else if (message.type === "execution-complete") { completed = true; cleanup(); - resolve({ success: true, console: logs, files, returnValue: message.returnValue }); - return true; + resolve({ + success: true, + console: consoleProvider.getLogs(), + files, + returnValue: message.returnValue, + }); } else if (message.type === "execution-error") { completed = true; cleanup(); - resolve({ success: false, console: logs, error: message.error, files }); - return true; + resolve({ success: false, console: consoleProvider.getLogs(), error: message.error, files }); } - return false; }, }; @@ -232,7 +227,7 @@ export class SandboxIframe extends LitElement { cleanup(); resolve({ success: false, - console: logs, + console: consoleProvider.getLogs(), error: { message: "Execution timeout (30s)", stack: "" }, files, }); @@ -347,7 +342,7 @@ export class SandboxIframe extends LitElement { await window.complete(null, returnValue); } catch (error) { - + // Call completion callbacks before complete() (error path) if (window.__completionCallbacks && window.__completionCallbacks.length > 0) { try { diff --git a/packages/web-ui/src/components/sandbox/ArtifactsRuntimeProvider.ts b/packages/web-ui/src/components/sandbox/ArtifactsRuntimeProvider.ts index a2947537..2301ca07 100644 --- a/packages/web-ui/src/components/sandbox/ArtifactsRuntimeProvider.ts +++ b/packages/web-ui/src/components/sandbox/ArtifactsRuntimeProvider.ts @@ -143,9 +143,9 @@ export class ArtifactsRuntimeProvider implements SandboxRuntimeProvider { }; } - async handleMessage(message: any, respond: (response: any) => void): Promise { + async handleMessage(message: any, respond: (response: any) => void): Promise { if (message.type !== "artifact-operation") { - return false; + return; } const { action, filename, content, mimeType } = message; @@ -224,11 +224,8 @@ export class ArtifactsRuntimeProvider implements SandboxRuntimeProvider { default: respond({ success: false, error: `Unknown artifact action: ${action}` }); } - - return true; } catch (error: any) { respond({ success: false, error: error.message }); - return true; } } diff --git a/packages/web-ui/src/components/sandbox/ConsoleRuntimeProvider.ts b/packages/web-ui/src/components/sandbox/ConsoleRuntimeProvider.ts index 622d8085..908d46eb 100644 --- a/packages/web-ui/src/components/sandbox/ConsoleRuntimeProvider.ts +++ b/packages/web-ui/src/components/sandbox/ConsoleRuntimeProvider.ts @@ -25,7 +25,7 @@ export class ConsoleRuntimeProvider implements SandboxRuntimeProvider { getRuntime(): (sandboxId: string) => void { return (_sandboxId: string) => { - // Console capture with immediate send + completion batch pattern + // Console capture with immediate send pattern const originalConsole = { log: console.log, error: console.error, @@ -33,8 +33,8 @@ export class ConsoleRuntimeProvider implements SandboxRuntimeProvider { info: console.info, }; - // Collect logs locally, send at completion - const collectedLogs: Array<{ method: string; text: string; args: any[] }> = []; + // Track pending send promises to wait for them in onCompleted + const pendingSends: Promise[] = []; ["log", "error", "warn", "info"].forEach((method) => { (console as any)[method] = (...args: any[]) => { @@ -48,29 +48,30 @@ export class ConsoleRuntimeProvider implements SandboxRuntimeProvider { }) .join(" "); - // Collect log for batch send at completion - collectedLogs.push({ method, text, args }); - // Always log locally too (originalConsole as any)[method].apply(console, args); + + // Send immediately and track the promise + if ((window as any).sendRuntimeMessage) { + const sendPromise = (window as any) + .sendRuntimeMessage({ + type: "console", + method, + text, + args, + }) + .catch(() => {}); + pendingSends.push(sendPromise); + } }; }); - // Register completion callback to send all collected logs + // Register completion callback to wait for all pending sends if ((window as any).onCompleted) { (window as any).onCompleted(async (_success: boolean) => { - // Send all collected logs - if (collectedLogs.length > 0 && (window as any).sendRuntimeMessage) { - await Promise.all( - collectedLogs.map((logEntry) => - (window as any).sendRuntimeMessage({ - type: "console", - method: logEntry.method, - text: logEntry.text, - args: logEntry.args, - }), - ), - ); + // Wait for all pending console sends to complete + if (pendingSends.length > 0) { + await Promise.all(pendingSends); } }); } @@ -78,7 +79,8 @@ export class ConsoleRuntimeProvider implements SandboxRuntimeProvider { // Track errors for HTML artifacts let lastError: { message: string; stack: string } | null = null; - // Error handlers + // Error handlers - track errors but don't log them + // (they'll be shown via execution-error message) window.addEventListener("error", (e) => { const text = (e.error?.stack || e.message || String(e)) + " at line " + (e.lineno || "?") + ":" + (e.colno || "?"); @@ -87,16 +89,6 @@ export class ConsoleRuntimeProvider implements SandboxRuntimeProvider { message: e.error?.message || e.message || String(e), stack: e.error?.stack || text, }; - - if ((window as any).sendRuntimeMessage) { - (window as any) - .sendRuntimeMessage({ - type: "console", - method: "error", - text, - }) - .catch(() => {}); - } }); window.addEventListener("unhandledrejection", (e) => { @@ -106,16 +98,6 @@ export class ConsoleRuntimeProvider implements SandboxRuntimeProvider { message: e.reason?.message || String(e.reason) || "Unhandled promise rejection", stack: e.reason?.stack || text, }; - - if ((window as any).sendRuntimeMessage) { - (window as any) - .sendRuntimeMessage({ - type: "console", - method: "error", - text, - }) - .catch(() => {}); - } }); // Expose complete() method for user code to call @@ -143,7 +125,7 @@ export class ConsoleRuntimeProvider implements SandboxRuntimeProvider { }; } - async handleMessage(message: any, respond: (response: any) => void): Promise { + async handleMessage(message: any, respond: (response: any) => void): Promise { if (message.type === "console") { // Collect console output this.logs.push({ @@ -160,10 +142,7 @@ export class ConsoleRuntimeProvider implements SandboxRuntimeProvider { }); // Acknowledge receipt respond({ success: true }); - return true; } - - return false; } /** diff --git a/packages/web-ui/src/components/sandbox/FileDownloadRuntimeProvider.ts b/packages/web-ui/src/components/sandbox/FileDownloadRuntimeProvider.ts index 2a105efc..beb74b91 100644 --- a/packages/web-ui/src/components/sandbox/FileDownloadRuntimeProvider.ts +++ b/packages/web-ui/src/components/sandbox/FileDownloadRuntimeProvider.ts @@ -77,20 +77,17 @@ export class FileDownloadRuntimeProvider implements SandboxRuntimeProvider { }; } - async handleMessage(message: any, respond: (response: any) => void): Promise { - if (message.type !== "file-returned") { - return false; + async handleMessage(message: any, respond: (response: any) => void): Promise { + if (message.type === "file-returned") { + // Collect file for caller + this.files.push({ + fileName: message.fileName, + content: message.content, + mimeType: message.mimeType, + }); + + respond({ success: true }); } - - // Collect file for caller - this.files.push({ - fileName: message.fileName, - content: message.content, - mimeType: message.mimeType, - }); - - respond({ success: true }); - return true; } /** diff --git a/packages/web-ui/src/components/sandbox/RuntimeMessageRouter.ts b/packages/web-ui/src/components/sandbox/RuntimeMessageRouter.ts index 5d5096b0..fbc47b94 100644 --- a/packages/web-ui/src/components/sandbox/RuntimeMessageRouter.ts +++ b/packages/web-ui/src/components/sandbox/RuntimeMessageRouter.ts @@ -9,9 +9,9 @@ declare const chrome: any; export interface MessageConsumer { /** * Handle a message from a sandbox. - * @returns true if message was consumed (stops propagation), false otherwise + * All consumers receive all messages - decide internally what to handle. */ - handleMessage(message: any): Promise; + handleMessage(message: any): Promise; } /** @@ -59,7 +59,7 @@ export class RuntimeMessageRouter { // Setup global listener if not already done this.setupListener(); - console.log("Registered sandbox:", sandboxId); + console.log(`Registered sandbox: ${sandboxId}, providers: ${providers.length}, consumers: ${consumers.length}`); } /** @@ -132,10 +132,20 @@ export class RuntimeMessageRouter { const { sandboxId, messageId } = e.data; if (!sandboxId) return; - console.log("Router received message for sandbox:", sandboxId, e.data); + console.log( + "[ROUTER] Received message for sandbox:", + sandboxId, + "type:", + e.data.type, + "full message:", + e.data, + ); const context = this.sandboxes.get(sandboxId); - if (!context) return; + if (!context) { + console.log("[ROUTER] No context found for sandbox:", sandboxId); + return; + } // Create respond() function for bidirectional communication const respond = (response: any) => { @@ -151,15 +161,19 @@ export class RuntimeMessageRouter { }; // 1. Try provider handlers first (for bidirectional comm) + console.log("[ROUTER] Broadcasting to", context.providers.length, "providers"); for (const provider of context.providers) { if (provider.handleMessage) { + console.log("[ROUTER] Calling provider.handleMessage for", provider.constructor.name); await provider.handleMessage(e.data, respond); // Don't stop - let consumers also handle the message } } // 2. Broadcast to consumers (one-way messages or lifecycle events) + console.log("[ROUTER] Broadcasting to", context.consumers.size, "consumers"); for (const consumer of context.consumers) { + console.log("[ROUTER] Calling consumer.handleMessage"); await consumer.handleMessage(e.data); // Don't stop - let all consumers see the message } @@ -194,17 +208,18 @@ export class RuntimeMessageRouter { // Route to providers (async) (async () => { + // 1. Try provider handlers first (for bidirectional comm) for (const provider of context.providers) { if (provider.handleMessage) { - const handled = await provider.handleMessage(message, respond); - if (handled) return; + await provider.handleMessage(message, respond); + // Don't stop - let consumers also handle the message } } - // Broadcast to consumers + // 2. Broadcast to consumers (one-way messages or lifecycle events) for (const consumer of context.consumers) { - const consumed = await consumer.handleMessage(message); - if (consumed) break; + await consumer.handleMessage(message); + // Don't stop - let all consumers see the message } })(); diff --git a/packages/web-ui/src/components/sandbox/SandboxRuntimeProvider.ts b/packages/web-ui/src/components/sandbox/SandboxRuntimeProvider.ts index dd0831aa..a63a6b48 100644 --- a/packages/web-ui/src/components/sandbox/SandboxRuntimeProvider.ts +++ b/packages/web-ui/src/components/sandbox/SandboxRuntimeProvider.ts @@ -20,13 +20,12 @@ export interface SandboxRuntimeProvider { /** * Optional message handler for bidirectional communication. - * Return true if the message was handled, false to let other handlers try. + * All providers receive all messages - decide internally what to handle. * * @param message - The message from the sandbox * @param respond - Function to send a response back to the sandbox - * @returns true if message was handled, false otherwise */ - handleMessage?(message: any, respond: (response: any) => void): Promise; + handleMessage?(message: any, respond: (response: any) => void): Promise; /** * Optional documentation describing what globals/functions this provider injects. diff --git a/packages/web-ui/src/tools/artifacts/HtmlArtifact.ts b/packages/web-ui/src/tools/artifacts/HtmlArtifact.ts index 2f01a355..21703b69 100644 --- a/packages/web-ui/src/tools/artifacts/HtmlArtifact.ts +++ b/packages/web-ui/src/tools/artifacts/HtmlArtifact.ts @@ -86,7 +86,7 @@ export class HtmlArtifact extends ArtifactElement { // Create consumer for console messages const consumer: MessageConsumer = { - handleMessage: async (message: any): Promise => { + handleMessage: async (message: any): Promise => { if (message.type === "console") { // Create new array reference for Lit reactivity this.logs = [ @@ -97,9 +97,7 @@ export class HtmlArtifact extends ArtifactElement { }, ]; this.requestUpdate(); // Re-render to show console - return true; } - return false; }, }; diff --git a/packages/web-ui/src/tools/javascript-repl.ts b/packages/web-ui/src/tools/javascript-repl.ts index 00a14b32..7d1cbd81 100644 --- a/packages/web-ui/src/tools/javascript-repl.ts +++ b/packages/web-ui/src/tools/javascript-repl.ts @@ -44,25 +44,26 @@ export async function executeJavaScript( // Remove the sandbox iframe after execution sandbox.remove(); - // Return plain text output - if (!result.success) { - // Return error as plain text - return { - output: `Error: ${result.error?.message || "Unknown error"}\n${result.error?.stack || ""}`, - }; - } - // Build plain text response let output = ""; // Add console output - result.console contains { type: string, text: string } from sandbox.js if (result.console && result.console.length > 0) { for (const entry of result.console) { - const prefix = entry.type === "error" ? "[ERROR]" : ""; - output += (prefix ? `${prefix} ` : "") + entry.text + "\n"; + output += entry.text + "\n"; } } + // Add error if execution failed + if (!result.success) { + if (output) output += "\n"; + output += `Error: ${result.error?.message || "Unknown error"}\n${result.error?.stack || ""}`; + + return { + output: output.trim(), + }; + } + // Add return value if present if (result.returnValue !== undefined) { if (output) output += "\n";