Restructuring and refactoring

This commit is contained in:
Mario Zechner 2025-10-03 02:15:37 +02:00
parent 3331701e7e
commit 79dd23b6da
31 changed files with 1088 additions and 1686 deletions

View file

@ -57,44 +57,45 @@ export class HtmlArtifact extends ArtifactElement {
this._content = value;
if (oldValue !== value) {
this.requestUpdate();
// Update sandbox iframe if it exists
if (this.sandboxIframeRef.value) {
// Execute content in sandbox if it exists
if (this.sandboxIframeRef.value && value) {
this.logs = [];
if (this.consoleLogsRef.value) {
this.consoleLogsRef.value.innerHTML = "";
}
this.updateConsoleButton();
this.sandboxIframeRef.value.updateContent(value);
this.executeContent(value);
}
}
}
private async executeContent(html: string) {
const sandbox = this.sandboxIframeRef.value;
if (!sandbox) return;
try {
const sandboxId = `artifact-${Date.now()}`;
const result = await sandbox.execute(sandboxId, html, this.attachments);
// Update logs with proper type casting
this.logs = (result.console || []).map((log) => ({
type: log.type === "error" ? ("error" as const) : ("log" as const),
text: log.text,
}));
this.updateConsoleButton();
} catch (error) {
console.error("HTML artifact execution failed:", error);
}
}
override get content(): string {
return this._content;
}
private handleConsoleEvent = (e: CustomEvent) => {
this.addLog(e.detail);
};
private handleExecutionComplete = (e: CustomEvent) => {
// Store final logs
this.logs = e.detail.logs || [];
this.updateConsoleButton();
};
private addLog(log: { type: "log" | "error"; text: string }) {
this.logs.push(log);
// Update console button text
this.updateConsoleButton();
// If console is open, append to DOM directly
if (this.consoleOpen && this.consoleLogsRef.value) {
const logEl = document.createElement("div");
logEl.className = `text-xs font-mono ${log.type === "error" ? "text-destructive" : "text-muted-foreground"}`;
logEl.textContent = `[${log.type}] ${log.text}`;
this.consoleLogsRef.value.appendChild(logEl);
override firstUpdated() {
// Execute initial content
if (this._content && this.sandboxIframeRef.value) {
this.executeContent(this._content);
}
}
@ -142,15 +143,7 @@ export class HtmlArtifact extends ArtifactElement {
<div class="flex-1 overflow-hidden relative">
<!-- Preview container - always in DOM, just hidden when not active -->
<div class="absolute inset-0 flex flex-col" style="display: ${this.viewMode === "preview" ? "flex" : "none"}">
<sandbox-iframe
class="flex-1"
.content=${this._content}
.artifactId=${this.filename}
.attachments=${this.attachments}
@console=${this.handleConsoleEvent}
@execution-complete=${this.handleExecutionComplete}
${ref(this.sandboxIframeRef)}
></sandbox-iframe>
<sandbox-iframe class="flex-1" ${ref(this.sandboxIframeRef)}></sandbox-iframe>
${
this.logs.length > 0
? html`

View file

@ -283,6 +283,10 @@ For text/html artifacts:
- For addons: import from 'https://esm.sh/three/examples/jsm/controls/OrbitControls.js'
- No localStorage/sessionStorage - use in-memory variables only
- CSS should be included inline
- CRITICAL REMINDER FOR HTML ARTIFACTS:
- ALWAYS set a background color inline in <style> or directly on body element
- Failure to set a background color is a COMPLIANCE ERROR
- Background color MUST be explicitly defined to ensure visibility and proper rendering
- Can embed base64 images directly in img tags
- Ensure the layout is responsive as the iframe might be resized
- Note: Network errors (404s) for external scripts may not be captured in logs due to browser security
@ -299,7 +303,14 @@ For text/markdown:
For image/svg+xml:
- Complete SVG markup
- Will be rendered inline
- Can embed raster images as base64 in SVG`,
- Can embed raster images as base64 in SVG
CRITICAL REMINDER FOR ALL ARTIFACTS:
- Prefer to update existing files rather than creating new ones
- Keep filenames consistent and descriptive
- Use appropriate file extensions
- Ensure HTML artifacts have a defined background color
`,
parameters: artifactsParamsSchema,
// Execute mutates our local store and returns a plain output
execute: async (_toolCallId: string, args: Static<typeof artifactsParamsSchema>, _signal?: AbortSignal) => {
@ -696,6 +707,9 @@ For image/svg+xml:
this.requestUpdate();
}
// Show the artifact
this.showArtifact(params.filename);
// For HTML files, wait for execution
let result = `Updated file ${params.filename}`;
if (this.getFileType(params.filename) === "html" && !options.skipWait) {
@ -731,6 +745,9 @@ For image/svg+xml:
this.onArtifactsChange?.();
}
// Show the artifact
this.showArtifact(params.filename);
// For HTML files, wait for execution
let result = `Rewrote file ${params.filename}`;
if (this.getFileType(params.filename) === "html" && !options.skipWait) {

View file

@ -1,7 +1,7 @@
import { html, type TemplateResult } from "@mariozechner/mini-lit";
import type { AgentTool, ToolResultMessage } from "@mariozechner/pi-ai";
import { type Static, Type } from "@sinclair/typebox";
import "../ConsoleBlock.js"; // Ensure console-block is registered
import "../components/ConsoleBlock.js"; // Ensure console-block is registered
import type { Attachment } from "../utils/attachment-utils.js";
import { registerToolRenderer } from "./renderer-registry.js";
import type { ToolRenderer } from "./types.js";

View file

@ -1,143 +1,19 @@
import { html, type TemplateResult } from "@mariozechner/mini-lit";
import type { AgentTool, ToolResultMessage } from "@mariozechner/pi-ai";
import { type Static, Type } from "@sinclair/typebox";
import { type SandboxFile, SandboxIframe, type SandboxResult } from "../components/SandboxedIframe.js";
import type { Attachment } from "../utils/attachment-utils.js";
import { registerToolRenderer } from "./renderer-registry.js";
import type { ToolRenderer } from "./types.js";
import "../ConsoleBlock.js"; // Ensure console-block is registered
import "../components/ConsoleBlock.js"; // Ensure console-block is registered
// Core JavaScript REPL execution logic without UI dependencies
export interface ReplExecuteResult {
success: boolean;
console?: Array<{ type: string; args: any[] }>;
files?: Array<{ fileName: string; content: string | Uint8Array; mimeType: string }>;
error?: { message: string; stack: string };
}
export class ReplExecutor {
private iframe: HTMLIFrameElement;
private ready: boolean = false;
private attachments: any[] = [];
// biome-ignore lint/complexity/noBannedTypes: fine here
private currentExecution: { resolve: Function; reject: Function } | null = null;
constructor(attachments: any[]) {
this.attachments = attachments;
this.iframe = this.createIframe();
this.setupMessageHandler();
this.initialize();
}
private createIframe(): HTMLIFrameElement {
const iframe = document.createElement("iframe");
// Use the sandboxed page from the manifest
iframe.src = chrome.runtime.getURL("sandbox.html");
iframe.style.display = "none";
document.body.appendChild(iframe);
return iframe;
}
private setupMessageHandler() {
const handler = (event: MessageEvent) => {
if (event.source !== this.iframe.contentWindow) return;
if (event.data.type === "ready") {
this.ready = true;
} else if (event.data.type === "result" && this.currentExecution) {
const { resolve } = this.currentExecution;
this.currentExecution = null;
resolve(event.data);
this.cleanup();
} else if (event.data.type === "error" && this.currentExecution) {
const { resolve } = this.currentExecution;
this.currentExecution = null;
resolve({
success: false,
error: event.data.error,
console: event.data.console || [],
});
this.cleanup();
}
};
window.addEventListener("message", handler);
// Store handler reference for cleanup
(this.iframe as any).__messageHandler = handler;
}
private initialize() {
// Send attachments once iframe is loaded
this.iframe.onload = () => {
setTimeout(() => {
this.iframe.contentWindow?.postMessage(
{
type: "setAttachments",
attachments: this.attachments,
},
"*",
);
}, 100);
};
}
cleanup() {
// Remove message handler
const handler = (this.iframe as any).__messageHandler;
if (handler) {
window.removeEventListener("message", handler);
}
// Remove iframe
this.iframe.remove();
// If there's a pending execution, reject it
if (this.currentExecution) {
this.currentExecution.reject(new Error("Execution aborted"));
this.currentExecution = null;
}
}
async execute(code: string): Promise<ReplExecuteResult> {
return new Promise((resolve, reject) => {
this.currentExecution = { resolve, reject };
// Wait for iframe to be ready
const checkReady = () => {
if (this.ready) {
this.iframe.contentWindow?.postMessage(
{
type: "execute",
code: code,
},
"*",
);
} else {
setTimeout(checkReady, 10);
}
};
checkReady();
// Timeout after 30 seconds
setTimeout(() => {
if (this.currentExecution?.resolve === resolve) {
this.currentExecution = null;
resolve({
success: false,
error: { message: "Execution timeout (30s)", stack: "" },
});
this.cleanup();
}
}, 30000);
});
}
}
// Execute JavaScript code with attachments
// Execute JavaScript code with attachments using SandboxedIframe
export async function executeJavaScript(
code: string,
attachments: any[] = [],
signal?: AbortSignal,
): Promise<{ output: string; files?: Array<{ fileName: string; content: any; mimeType: string }> }> {
): Promise<{ output: string; files?: SandboxFile[] }> {
if (!code) {
throw new Error("Code parameter is required");
}
@ -147,35 +23,34 @@ export async function executeJavaScript(
throw new Error("Execution aborted");
}
// Create a one-shot executor
const executor = new ReplExecutor(attachments);
// Listen for abort signal
const abortHandler = () => {
executor.cleanup();
};
signal?.addEventListener("abort", abortHandler);
// Create a SandboxedIframe instance for execution
const sandbox = new SandboxIframe();
sandbox.style.display = "none";
document.body.appendChild(sandbox);
try {
const result = await executor.execute(code);
const sandboxId = `repl-${Date.now()}`;
const result: SandboxResult = await sandbox.execute(sandboxId, code, attachments, signal);
// 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 || ""}`,
output: `Error: ${result.error?.message || "Unknown error"}\n${result.error?.stack || ""}`,
};
}
// Build plain text response
let output = "";
// Add console 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]" : entry.type === "warn" ? "[WARN]" : "";
const line = prefix ? `${prefix} ${entry.args.join(" ")}` : entry.args.join(" ");
output += line + "\n";
const prefix = entry.type === "error" ? "[ERROR]" : "";
output += (prefix ? `${prefix} ` : "") + entry.text + "\n";
}
}
@ -197,9 +72,9 @@ export async function executeJavaScript(
files: result.files,
};
} catch (error: any) {
// Clean up on error
sandbox.remove();
throw new Error(error.message || "Execution failed");
} finally {
signal?.removeEventListener("abort", abortHandler);
}
}