Files
my-pi/packages/web-ui/src/components/sandbox/ArtifactsRuntimeProvider.ts
T
Mario Zechner 7a859cbe52 Merge createArtifact and updateArtifact into createOrUpdateArtifact
LLMs get confused about when to use create vs update. The single function
automatically detects if the artifact exists and chooses the right operation.

Changes:
- Replace createArtifact/updateArtifact with createOrUpdateArtifact in runtime
- Update handler to check existence and use appropriate command (create/rewrite)
- Simplify prompt documentation and examples
2025-10-10 01:43:36 +02:00

217 lines
6.0 KiB
TypeScript

import { ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION } from "../../prompts/tool-prompts.js";
import type { SandboxRuntimeProvider } from "./SandboxRuntimeProvider.js";
// Define minimal interface for ArtifactsPanel to avoid circular dependencies
interface ArtifactsPanelLike {
artifacts: Map<string, { content: string }>;
tool: {
execute(toolCallId: string, args: { command: string; filename: string; content?: string }): Promise<any>;
};
}
interface AgentLike {
appendMessage(message: any): void;
}
/**
* Artifacts Runtime Provider
*
* Provides programmatic access to session artifacts from sandboxed code.
* Allows code to create, read, update, and delete artifacts dynamically.
* Supports both online (extension) and offline (downloaded HTML) modes.
*/
export class ArtifactsRuntimeProvider implements SandboxRuntimeProvider {
constructor(
private artifactsPanel: ArtifactsPanelLike,
private agent?: AgentLike,
) {}
getData(): Record<string, any> {
// Inject artifact snapshot for offline mode
const snapshot: Record<string, string> = {};
this.artifactsPanel.artifacts.forEach((artifact, filename) => {
snapshot[filename] = artifact.content;
});
return { artifacts: snapshot };
}
getRuntime(): (sandboxId: string) => void {
// This function will be stringified, so no external references!
return (_sandboxId: string) => {
// Auto-parse/stringify for .json files
const isJsonFile = (filename: string) => filename.endsWith(".json");
(window as any).hasArtifact = async (filename: string): Promise<boolean> => {
// Online: ask extension
if ((window as any).sendRuntimeMessage) {
const response = await (window as any).sendRuntimeMessage({
type: "artifact-operation",
action: "has",
filename,
});
if (!response.success) throw new Error(response.error);
return response.result;
}
// Offline: check snapshot
else {
return !!(window as any).artifacts?.[filename];
}
};
(window as any).getArtifact = async (filename: string): Promise<any> => {
let content: string;
// Online: ask extension
if ((window as any).sendRuntimeMessage) {
const response = await (window as any).sendRuntimeMessage({
type: "artifact-operation",
action: "get",
filename,
});
if (!response.success) throw new Error(response.error);
content = response.result;
}
// Offline: read snapshot
else {
if (!(window as any).artifacts?.[filename]) {
throw new Error(`Artifact not found (offline mode): ${filename}`);
}
content = (window as any).artifacts[filename];
}
// Auto-parse .json files
if (isJsonFile(filename)) {
try {
return JSON.parse(content);
} catch (e) {
throw new Error(`Failed to parse JSON from ${filename}: ${e}`);
}
}
return content;
};
(window as any).createOrUpdateArtifact = async (
filename: string,
content: any,
mimeType?: string,
): Promise<void> => {
if (!(window as any).sendRuntimeMessage) {
throw new Error("Cannot create/update artifacts in offline mode (read-only)");
}
let finalContent = content;
// Auto-stringify .json files
if (isJsonFile(filename) && typeof content !== "string") {
finalContent = JSON.stringify(content, null, 2);
} else if (typeof content !== "string") {
finalContent = JSON.stringify(content, null, 2);
}
const response = await (window as any).sendRuntimeMessage({
type: "artifact-operation",
action: "createOrUpdate",
filename,
content: finalContent,
mimeType,
});
if (!response.success) throw new Error(response.error);
};
(window as any).deleteArtifact = async (filename: string): Promise<void> => {
if (!(window as any).sendRuntimeMessage) {
throw new Error("Cannot delete artifacts in offline mode (read-only)");
}
const response = await (window as any).sendRuntimeMessage({
type: "artifact-operation",
action: "delete",
filename,
});
if (!response.success) throw new Error(response.error);
};
};
}
async handleMessage(message: any, respond: (response: any) => void): Promise<void> {
if (message.type !== "artifact-operation") {
return;
}
const { action, filename, content, mimeType } = message;
try {
switch (action) {
case "has": {
const exists = this.artifactsPanel.artifacts.has(filename);
respond({ success: true, result: exists });
break;
}
case "get": {
const artifact = this.artifactsPanel.artifacts.get(filename);
if (!artifact) {
respond({ success: false, error: `Artifact not found: ${filename}` });
} else {
respond({ success: true, result: artifact.content });
}
break;
}
case "createOrUpdate": {
try {
const exists = this.artifactsPanel.artifacts.has(filename);
const command = exists ? "rewrite" : "create";
const action = exists ? "update" : "create";
await this.artifactsPanel.tool.execute("", {
command,
filename,
content,
});
this.agent?.appendMessage({
role: "artifact",
action,
filename,
content,
...(action === "create" && { title: filename }),
timestamp: new Date().toISOString(),
});
respond({ success: true });
} catch (err: any) {
respond({ success: false, error: err.message });
}
break;
}
case "delete": {
try {
await this.artifactsPanel.tool.execute("", {
command: "delete",
filename,
});
this.agent?.appendMessage({
role: "artifact",
action: "delete",
filename,
timestamp: new Date().toISOString(),
});
respond({ success: true });
} catch (err: any) {
respond({ success: false, error: err.message });
}
break;
}
default:
respond({ success: false, error: `Unknown artifact action: ${action}` });
}
} catch (error: any) {
respond({ success: false, error: error.message });
}
}
getDescription(): string {
return ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION;
}
}