mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-16 17:01:02 +00:00
- ConsoleRuntimeProvider was handling execution-complete and returning true - This stopped message propagation before executionConsumer could handle it - ExecutionConsumer never got execution-complete, so promise never resolved - Now ConsoleRuntimeProvider responds but returns false to allow propagation - Fixes javascript_repl never completing (30s timeout)
208 lines
5.3 KiB
TypeScript
208 lines
5.3 KiB
TypeScript
import type { SandboxRuntimeProvider } from "./SandboxRuntimeProvider.js";
|
|
|
|
export interface ConsoleLog {
|
|
type: "log" | "warn" | "error" | "info";
|
|
text: string;
|
|
args?: unknown[];
|
|
}
|
|
|
|
/**
|
|
* Console Runtime Provider
|
|
*
|
|
* REQUIRED provider that should always be included first.
|
|
* Provides console capture, error handling, and execution lifecycle management.
|
|
* Collects console output for retrieval by caller.
|
|
*/
|
|
export class ConsoleRuntimeProvider implements SandboxRuntimeProvider {
|
|
private logs: ConsoleLog[] = [];
|
|
private completionError: { message: string; stack: string } | null = null;
|
|
private completed = false;
|
|
|
|
getData(): Record<string, any> {
|
|
// No data needed
|
|
return {};
|
|
}
|
|
|
|
getRuntime(): (sandboxId: string) => void {
|
|
return (_sandboxId: string) => {
|
|
// Console capture with immediate send + completion batch pattern
|
|
const originalConsole = {
|
|
log: console.log,
|
|
error: console.error,
|
|
warn: console.warn,
|
|
info: console.info,
|
|
};
|
|
|
|
// Collect logs locally, send at completion
|
|
const collectedLogs: Array<{ method: string; text: string; args: any[] }> = [];
|
|
|
|
["log", "error", "warn", "info"].forEach((method) => {
|
|
(console as any)[method] = (...args: any[]) => {
|
|
const text = args
|
|
.map((arg) => {
|
|
try {
|
|
return typeof arg === "object" ? JSON.stringify(arg) : String(arg);
|
|
} catch {
|
|
return String(arg);
|
|
}
|
|
})
|
|
.join(" ");
|
|
|
|
// Collect log for batch send at completion
|
|
collectedLogs.push({ method, text, args });
|
|
|
|
// Always log locally too
|
|
(originalConsole as any)[method].apply(console, args);
|
|
};
|
|
});
|
|
|
|
// Register completion callback to send all collected logs
|
|
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,
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Track errors for HTML artifacts
|
|
let lastError: { message: string; stack: string } | null = null;
|
|
|
|
// Error handlers
|
|
window.addEventListener("error", (e) => {
|
|
const text =
|
|
(e.error?.stack || e.message || String(e)) + " at line " + (e.lineno || "?") + ":" + (e.colno || "?");
|
|
|
|
lastError = {
|
|
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) => {
|
|
const text = "Unhandled promise rejection: " + (e.reason?.message || e.reason || "Unknown error");
|
|
|
|
lastError = {
|
|
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
|
|
let completionSent = false;
|
|
(window as any).complete = async (error?: { message: string; stack: string }) => {
|
|
if (completionSent) return;
|
|
completionSent = true;
|
|
|
|
const finalError = error || lastError;
|
|
|
|
if ((window as any).sendRuntimeMessage) {
|
|
if (finalError) {
|
|
console.log("Reporting execution error:", finalError);
|
|
await (window as any).sendRuntimeMessage({
|
|
type: "execution-error",
|
|
error: finalError,
|
|
});
|
|
console.log("Execution completed");
|
|
} else {
|
|
console.log("Reporting execution complete");
|
|
await (window as any).sendRuntimeMessage({
|
|
type: "execution-complete",
|
|
});
|
|
console.log("Execution completed");
|
|
}
|
|
}
|
|
};
|
|
};
|
|
}
|
|
|
|
async handleMessage(message: any, respond: (response: any) => void): Promise<boolean> {
|
|
if (message.type === "console") {
|
|
// Collect console output
|
|
this.logs.push({
|
|
type:
|
|
message.method === "error"
|
|
? "error"
|
|
: message.method === "warn"
|
|
? "warn"
|
|
: message.method === "info"
|
|
? "info"
|
|
: "log",
|
|
text: message.text,
|
|
args: message.args,
|
|
});
|
|
// Acknowledge receipt
|
|
respond({ success: true });
|
|
return true;
|
|
}
|
|
|
|
// Don't handle execution-complete/error - let executionConsumer handle those
|
|
// We just need to respond to allow the message to proceed
|
|
if (message.type === "execution-complete" || message.type === "execution-error") {
|
|
respond({ success: true });
|
|
return false; // Don't stop propagation
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get collected console logs
|
|
*/
|
|
getLogs(): ConsoleLog[] {
|
|
return this.logs;
|
|
}
|
|
|
|
/**
|
|
* Get completion status
|
|
*/
|
|
isCompleted(): boolean {
|
|
return this.completed;
|
|
}
|
|
|
|
/**
|
|
* Get completion error if any
|
|
*/
|
|
getCompletionError(): { message: string; stack: string } | null {
|
|
return this.completionError;
|
|
}
|
|
|
|
/**
|
|
* Reset state for reuse
|
|
*/
|
|
reset(): void {
|
|
this.logs = [];
|
|
this.completionError = null;
|
|
this.completed = false;
|
|
}
|
|
}
|