mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-19 22:01:38 +00:00
Restructuring and refactoring
This commit is contained in:
parent
3331701e7e
commit
79dd23b6da
31 changed files with 1088 additions and 1686 deletions
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue