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";