mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-20 01:00:24 +00:00
Add artifact message persistence for session reconstruction
- Add ArtifactMessage type as core part of AppMessage union (not CustomMessages) - ArtifactsRuntimeProvider appends artifact messages on create/update/delete - MessageList filters out artifact messages (UI display only) - artifacts.ts reconstructFromMessages handles artifact messages - Export ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION from main index - Fix artifact creation bug: pass filename as title instead of mimeType Changes: - web-ui/src/components/Messages.ts: Add ArtifactMessage to BaseMessage union - web-ui/src/components/MessageList.ts: Skip artifact messages in render - web-ui/src/components/sandbox/ArtifactsRuntimeProvider.ts: Append messages, fix title parameter - web-ui/src/ChatPanel.ts: Pass agent.appendMessage callback - web-ui/src/tools/artifacts/artifacts.ts: Handle artifact messages in reconstructFromMessages - web-ui/src/index.ts: Export ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION - web-ui/example/src/custom-messages.ts: Update message transformer to filter artifacts
This commit is contained in:
parent
0eaa879d46
commit
4d2ca6ab2a
20 changed files with 669 additions and 239 deletions
|
|
@ -78,6 +78,11 @@ export function createSystemNotification(
|
||||||
export function customMessageTransformer(messages: AppMessage[]): Message[] {
|
export function customMessageTransformer(messages: AppMessage[]): Message[] {
|
||||||
return messages
|
return messages
|
||||||
.filter((m) => {
|
.filter((m) => {
|
||||||
|
// Filter out artifact messages - they're for session reconstruction only
|
||||||
|
if (m.role === "artifact") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Keep LLM-compatible messages + custom messages
|
// Keep LLM-compatible messages + custom messages
|
||||||
return (
|
return (
|
||||||
m.role === "user" ||
|
m.role === "user" ||
|
||||||
|
|
|
||||||
|
|
@ -194,7 +194,11 @@ Feel free to use these tools when needed to provide accurate and helpful respons
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await chatPanel.setAgent(agent);
|
await chatPanel.setAgent(agent, {
|
||||||
|
onApiKeyRequired: async (provider: string) => {
|
||||||
|
return await ApiKeyPromptDialog.prompt(provider);
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadSession = async (sessionId: string): Promise<boolean> => {
|
const loadSession = async (sessionId: string): Promise<boolean> => {
|
||||||
|
|
@ -377,9 +381,6 @@ async function initApp() {
|
||||||
|
|
||||||
// Create ChatPanel
|
// Create ChatPanel
|
||||||
chatPanel = new ChatPanel();
|
chatPanel = new ChatPanel();
|
||||||
chatPanel.onApiKeyRequired = async (provider: string) => {
|
|
||||||
return await ApiKeyPromptDialog.prompt(provider);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check for session in URL
|
// Check for session in URL
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import { Badge, html } from "@mariozechner/mini-lit";
|
import { Badge, html } from "@mariozechner/mini-lit";
|
||||||
import { LitElement } from "lit";
|
import { LitElement } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators.js";
|
import { customElement, state } from "lit/decorators.js";
|
||||||
import type { Agent } from "./agent/agent.js";
|
import type { Agent } from "./agent/agent.js";
|
||||||
import "./components/AgentInterface.js";
|
import "./components/AgentInterface.js";
|
||||||
|
import type { AgentTool } from "@mariozechner/pi-ai";
|
||||||
import type { AgentInterface } from "./components/AgentInterface.js";
|
import type { AgentInterface } from "./components/AgentInterface.js";
|
||||||
|
import { ArtifactsRuntimeProvider } from "./components/sandbox/ArtifactsRuntimeProvider.js";
|
||||||
import { AttachmentsRuntimeProvider } from "./components/sandbox/AttachmentsRuntimeProvider.js";
|
import { AttachmentsRuntimeProvider } from "./components/sandbox/AttachmentsRuntimeProvider.js";
|
||||||
import type { SandboxRuntimeProvider } from "./components/sandbox/SandboxRuntimeProvider.js";
|
import type { SandboxRuntimeProvider } from "./components/sandbox/SandboxRuntimeProvider.js";
|
||||||
import { ArtifactsPanel, ArtifactsToolRenderer } from "./tools/artifacts/index.js";
|
import { ArtifactsPanel, ArtifactsToolRenderer } from "./tools/artifacts/index.js";
|
||||||
|
|
@ -23,25 +25,6 @@ export class ChatPanel extends LitElement {
|
||||||
@state() private artifactCount = 0;
|
@state() private artifactCount = 0;
|
||||||
@state() private showArtifactsPanel = false;
|
@state() private showArtifactsPanel = false;
|
||||||
@state() private windowWidth = 0;
|
@state() private windowWidth = 0;
|
||||||
@property({ attribute: false }) runtimeProvidersFactory = () => {
|
|
||||||
const attachments: Attachment[] = [];
|
|
||||||
for (const message of this.agent!.state.messages) {
|
|
||||||
if (message.role === "user") {
|
|
||||||
message.attachments?.forEach((a) => {
|
|
||||||
attachments.push(a);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const providers: SandboxRuntimeProvider[] = [];
|
|
||||||
if (attachments.length > 0) {
|
|
||||||
providers.push(new AttachmentsRuntimeProvider(attachments));
|
|
||||||
}
|
|
||||||
return providers;
|
|
||||||
};
|
|
||||||
@property({ attribute: false }) sandboxUrlProvider?: () => string;
|
|
||||||
@property({ attribute: false }) onApiKeyRequired?: (provider: string) => Promise<boolean>;
|
|
||||||
@property({ attribute: false }) onBeforeSend?: () => void | Promise<void>;
|
|
||||||
@property({ attribute: false }) additionalTools?: any[];
|
|
||||||
|
|
||||||
private resizeHandler = () => {
|
private resizeHandler = () => {
|
||||||
this.windowWidth = window.innerWidth;
|
this.windowWidth = window.innerWidth;
|
||||||
|
|
@ -72,7 +55,19 @@ export class ChatPanel extends LitElement {
|
||||||
window.removeEventListener("resize", this.resizeHandler);
|
window.removeEventListener("resize", this.resizeHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setAgent(agent: Agent) {
|
async setAgent(
|
||||||
|
agent: Agent,
|
||||||
|
config?: {
|
||||||
|
onApiKeyRequired?: (provider: string) => Promise<boolean>;
|
||||||
|
onBeforeSend?: () => void | Promise<void>;
|
||||||
|
sandboxUrlProvider?: () => string;
|
||||||
|
toolsFactory?: (
|
||||||
|
agent: Agent,
|
||||||
|
agentInterface: AgentInterface,
|
||||||
|
artifactsPanel: ArtifactsPanel,
|
||||||
|
) => AgentTool<any>[];
|
||||||
|
},
|
||||||
|
) {
|
||||||
this.agent = agent;
|
this.agent = agent;
|
||||||
|
|
||||||
// Create AgentInterface
|
// Create AgentInterface
|
||||||
|
|
@ -82,26 +77,74 @@ export class ChatPanel extends LitElement {
|
||||||
this.agentInterface.enableModelSelector = true;
|
this.agentInterface.enableModelSelector = true;
|
||||||
this.agentInterface.enableThinkingSelector = true;
|
this.agentInterface.enableThinkingSelector = true;
|
||||||
this.agentInterface.showThemeToggle = false;
|
this.agentInterface.showThemeToggle = false;
|
||||||
this.agentInterface.onApiKeyRequired = this.onApiKeyRequired;
|
this.agentInterface.onApiKeyRequired = config?.onApiKeyRequired;
|
||||||
this.agentInterface.onBeforeSend = this.onBeforeSend;
|
this.agentInterface.onBeforeSend = config?.onBeforeSend;
|
||||||
|
|
||||||
// Create JavaScript REPL tool
|
// Create JavaScript REPL tool
|
||||||
const javascriptReplTool = createJavaScriptReplTool();
|
const javascriptReplTool = createJavaScriptReplTool();
|
||||||
if (this.sandboxUrlProvider) {
|
if (config?.sandboxUrlProvider) {
|
||||||
javascriptReplTool.sandboxUrlProvider = this.sandboxUrlProvider;
|
javascriptReplTool.sandboxUrlProvider = config.sandboxUrlProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up artifacts panel
|
// Set up artifacts panel
|
||||||
this.artifactsPanel = new ArtifactsPanel();
|
this.artifactsPanel = new ArtifactsPanel();
|
||||||
if (this.sandboxUrlProvider) {
|
if (config?.sandboxUrlProvider) {
|
||||||
this.artifactsPanel.sandboxUrlProvider = this.sandboxUrlProvider;
|
this.artifactsPanel.sandboxUrlProvider = config.sandboxUrlProvider;
|
||||||
}
|
}
|
||||||
// Register the standalone tool renderer (not the panel itself)
|
// Register the standalone tool renderer (not the panel itself)
|
||||||
registerToolRenderer("artifacts", new ArtifactsToolRenderer(this.artifactsPanel));
|
registerToolRenderer("artifacts", new ArtifactsToolRenderer(this.artifactsPanel));
|
||||||
|
|
||||||
// Runtime providers factory
|
// Runtime providers factory
|
||||||
javascriptReplTool.runtimeProvidersFactory = this.runtimeProvidersFactory;
|
const runtimeProvidersFactory = () => {
|
||||||
this.artifactsPanel.runtimeProvidersFactory = this.runtimeProvidersFactory;
|
const attachments: Attachment[] = [];
|
||||||
|
for (const message of this.agent!.state.messages) {
|
||||||
|
if (message.role === "user") {
|
||||||
|
message.attachments?.forEach((a) => {
|
||||||
|
attachments.push(a);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const providers: SandboxRuntimeProvider[] = [];
|
||||||
|
|
||||||
|
// Add attachments provider if there are attachments
|
||||||
|
if (attachments.length > 0) {
|
||||||
|
providers.push(new AttachmentsRuntimeProvider(attachments));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add artifacts provider (always available)
|
||||||
|
providers.push(
|
||||||
|
new ArtifactsRuntimeProvider(
|
||||||
|
() => this.artifactsPanel!.artifacts,
|
||||||
|
async (filename: string, content: string) => {
|
||||||
|
await this.artifactsPanel!.tool.execute("", {
|
||||||
|
command: "create",
|
||||||
|
filename,
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async (filename: string, content: string) => {
|
||||||
|
await this.artifactsPanel!.tool.execute("", {
|
||||||
|
command: "rewrite",
|
||||||
|
filename,
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async (filename: string) => {
|
||||||
|
await this.artifactsPanel!.tool.execute("", {
|
||||||
|
command: "delete",
|
||||||
|
filename,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(message: any) => {
|
||||||
|
this.agent!.appendMessage(message);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return providers;
|
||||||
|
};
|
||||||
|
javascriptReplTool.runtimeProvidersFactory = runtimeProvidersFactory;
|
||||||
|
this.artifactsPanel.runtimeProvidersFactory = runtimeProvidersFactory;
|
||||||
|
|
||||||
this.artifactsPanel.onArtifactsChange = () => {
|
this.artifactsPanel.onArtifactsChange = () => {
|
||||||
const count = this.artifactsPanel?.artifacts?.size ?? 0;
|
const count = this.artifactsPanel?.artifacts?.size ?? 0;
|
||||||
|
|
@ -125,7 +168,8 @@ export class ChatPanel extends LitElement {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set tools on the agent
|
// Set tools on the agent
|
||||||
const tools = [javascriptReplTool, this.artifactsPanel.tool, ...(this.additionalTools || [])];
|
const additionalTools = config?.toolsFactory?.(agent, this.agentInterface, this.artifactsPanel) || [];
|
||||||
|
const tools = [javascriptReplTool, this.artifactsPanel.tool, ...additionalTools];
|
||||||
this.agent.setTools(tools);
|
this.agent.setTools(tools);
|
||||||
|
|
||||||
// Reconstruct artifacts from existing messages
|
// Reconstruct artifacts from existing messages
|
||||||
|
|
|
||||||
|
|
@ -293,10 +293,10 @@ export class Agent {
|
||||||
this.patch({ isStreaming: false, streamMessage: null, pendingToolCalls: new Set<string>() });
|
this.patch({ isStreaming: false, streamMessage: null, pendingToolCalls: new Set<string>() });
|
||||||
this.abortController = undefined;
|
this.abortController = undefined;
|
||||||
}
|
}
|
||||||
/*{
|
{
|
||||||
const { systemPrompt, model, messages } = this._state;
|
const { systemPrompt, model, messages } = this._state;
|
||||||
console.log("final state:", { systemPrompt, model, messages });
|
console.log("final state:", { systemPrompt, model, messages });
|
||||||
}*/
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private patch(p: Partial<AgentState>): void {
|
private patch(p: Partial<AgentState>): void {
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,11 @@ export class MessageList extends LitElement {
|
||||||
const items: Array<{ key: string; template: TemplateResult }> = [];
|
const items: Array<{ key: string; template: TemplateResult }> = [];
|
||||||
let index = 0;
|
let index = 0;
|
||||||
for (const msg of this.messages) {
|
for (const msg of this.messages) {
|
||||||
|
// Skip artifact messages - they're for session persistence only, not UI display
|
||||||
|
if (msg.role === "artifact") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Try custom renderer first
|
// Try custom renderer first
|
||||||
const customTemplate = renderMessage(msg);
|
const customTemplate = renderMessage(msg);
|
||||||
if (customTemplate) {
|
if (customTemplate) {
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,18 @@ import { i18n } from "../utils/i18n.js";
|
||||||
|
|
||||||
export type UserMessageWithAttachments = UserMessageType & { attachments?: Attachment[] };
|
export type UserMessageWithAttachments = UserMessageType & { attachments?: Attachment[] };
|
||||||
|
|
||||||
|
// Artifact message type for session persistence
|
||||||
|
export interface ArtifactMessage {
|
||||||
|
role: "artifact";
|
||||||
|
action: "create" | "update" | "delete";
|
||||||
|
filename: string;
|
||||||
|
content?: string;
|
||||||
|
title?: string;
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Base message union
|
// Base message union
|
||||||
type BaseMessage = AssistantMessageType | UserMessageWithAttachments | ToolResultMessageType;
|
type BaseMessage = AssistantMessageType | UserMessageWithAttachments | ToolResultMessageType | ArtifactMessage;
|
||||||
|
|
||||||
// Extensible interface - apps can extend via declaration merging
|
// Extensible interface - apps can extend via declaration merging
|
||||||
// Example:
|
// Example:
|
||||||
|
|
|
||||||
|
|
@ -169,7 +169,7 @@ export class SandboxIframe extends LitElement {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// 4. Create execution consumer for lifecycle messages
|
// 4. Create execution consumer for lifecycle messages
|
||||||
const executionConsumer: MessageConsumer = {
|
const executionConsumer: MessageConsumer = {
|
||||||
handleMessage(message: any): boolean {
|
async handleMessage(message: any): Promise<boolean> {
|
||||||
if (message.type === "console") {
|
if (message.type === "console") {
|
||||||
logs.push({
|
logs.push({
|
||||||
type: message.method === "error" ? "error" : "log",
|
type: message.method === "error" ? "error" : "log",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,232 @@
|
||||||
|
import { ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION } from "../../prompts/tool-prompts.js";
|
||||||
|
import type { SandboxRuntimeProvider } from "./SandboxRuntimeProvider.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Artifacts Runtime Provider
|
||||||
|
*
|
||||||
|
* Provides programmatic access to session artifacts from sandboxed code.
|
||||||
|
* Allows code to create, read, update, and delete artifacts dynamically.
|
||||||
|
*/
|
||||||
|
export class ArtifactsRuntimeProvider implements SandboxRuntimeProvider {
|
||||||
|
constructor(
|
||||||
|
private getArtifactsFn: () => Map<string, { content: string }>,
|
||||||
|
private createArtifactFn: (filename: string, content: string, title?: string) => Promise<void>,
|
||||||
|
private updateArtifactFn: (filename: string, content: string, title?: string) => Promise<void>,
|
||||||
|
private deleteArtifactFn: (filename: string) => Promise<void>,
|
||||||
|
private appendMessageFn?: (message: any) => void,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
getData(): Record<string, any> {
|
||||||
|
// No initial data injection needed - artifacts are accessed via async functions
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
getRuntime(): (sandboxId: string) => void {
|
||||||
|
// This function will be stringified, so no external references!
|
||||||
|
return (sandboxId: string) => {
|
||||||
|
// Helper to send message and wait for response
|
||||||
|
const sendArtifactMessage = (action: string, data: any): Promise<any> => {
|
||||||
|
console.log("Sending artifact message:", action, data);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const messageId = `artifact_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
||||||
|
|
||||||
|
const handler = (event: MessageEvent) => {
|
||||||
|
if (event.data.type === "artifact-response" && event.data.messageId === messageId) {
|
||||||
|
window.removeEventListener("message", handler);
|
||||||
|
if (event.data.success) {
|
||||||
|
resolve(event.data.result);
|
||||||
|
} else {
|
||||||
|
reject(new Error(event.data.error || "Artifact operation failed"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("message", handler);
|
||||||
|
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
type: "artifact-operation",
|
||||||
|
sandboxId,
|
||||||
|
messageId,
|
||||||
|
action,
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
"*",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-parse/stringify for .json files
|
||||||
|
const isJsonFile = (filename: string) => filename.endsWith(".json");
|
||||||
|
|
||||||
|
(window as any).hasArtifact = async (filename: string): Promise<boolean> => {
|
||||||
|
return await sendArtifactMessage("has", { filename });
|
||||||
|
};
|
||||||
|
|
||||||
|
(window as any).getArtifact = async (filename: string): Promise<any> => {
|
||||||
|
const content = await sendArtifactMessage("get", { 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).createArtifact = async (filename: string, content: any, mimeType?: string): Promise<void> => {
|
||||||
|
let finalContent = content;
|
||||||
|
let finalMimeType = mimeType;
|
||||||
|
|
||||||
|
// Auto-stringify .json files
|
||||||
|
if (isJsonFile(filename) && typeof content !== "string") {
|
||||||
|
finalContent = JSON.stringify(content, null, 2);
|
||||||
|
finalMimeType = mimeType || "application/json";
|
||||||
|
} else if (typeof content === "string") {
|
||||||
|
finalContent = content;
|
||||||
|
finalMimeType = mimeType || "text/plain";
|
||||||
|
} else {
|
||||||
|
finalContent = JSON.stringify(content, null, 2);
|
||||||
|
finalMimeType = mimeType || "application/json";
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendArtifactMessage("create", { filename, content: finalContent, mimeType: finalMimeType });
|
||||||
|
};
|
||||||
|
|
||||||
|
(window as any).updateArtifact = async (filename: string, content: any, mimeType?: string): Promise<void> => {
|
||||||
|
let finalContent = content;
|
||||||
|
let finalMimeType = mimeType;
|
||||||
|
|
||||||
|
// Auto-stringify .json files
|
||||||
|
if (isJsonFile(filename) && typeof content !== "string") {
|
||||||
|
finalContent = JSON.stringify(content, null, 2);
|
||||||
|
finalMimeType = mimeType || "application/json";
|
||||||
|
} else if (typeof content === "string") {
|
||||||
|
finalContent = content;
|
||||||
|
finalMimeType = mimeType || "text/plain";
|
||||||
|
} else {
|
||||||
|
finalContent = JSON.stringify(content, null, 2);
|
||||||
|
finalMimeType = mimeType || "application/json";
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendArtifactMessage("update", { filename, content: finalContent, mimeType: finalMimeType });
|
||||||
|
};
|
||||||
|
|
||||||
|
(window as any).deleteArtifact = async (filename: string): Promise<void> => {
|
||||||
|
await sendArtifactMessage("delete", { filename });
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleMessage(message: any, respond: (response: any) => void): Promise<boolean> {
|
||||||
|
if (message.type !== "artifact-operation") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { action, data, messageId } = message;
|
||||||
|
|
||||||
|
const sendResponse = (success: boolean, result?: any, error?: string) => {
|
||||||
|
respond({
|
||||||
|
type: "artifact-response",
|
||||||
|
messageId,
|
||||||
|
success,
|
||||||
|
result,
|
||||||
|
error,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (action) {
|
||||||
|
case "has": {
|
||||||
|
const artifacts = this.getArtifactsFn();
|
||||||
|
const exists = artifacts.has(data.filename);
|
||||||
|
sendResponse(true, exists);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "get": {
|
||||||
|
const artifacts = this.getArtifactsFn();
|
||||||
|
const artifact = artifacts.get(data.filename);
|
||||||
|
if (!artifact) {
|
||||||
|
sendResponse(false, undefined, `Artifact not found: ${data.filename}`);
|
||||||
|
} else {
|
||||||
|
sendResponse(true, artifact.content);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "create": {
|
||||||
|
try {
|
||||||
|
// Note: mimeType parameter is ignored - artifact type is inferred from filename extension
|
||||||
|
// Third parameter is title, defaults to filename
|
||||||
|
await this.createArtifactFn(data.filename, data.content, data.filename);
|
||||||
|
// Append artifact message for session persistence
|
||||||
|
this.appendMessageFn?.({
|
||||||
|
role: "artifact",
|
||||||
|
action: "create",
|
||||||
|
filename: data.filename,
|
||||||
|
content: data.content,
|
||||||
|
title: data.filename,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
sendResponse(true);
|
||||||
|
} catch (err: any) {
|
||||||
|
sendResponse(false, undefined, err.message);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "update": {
|
||||||
|
try {
|
||||||
|
// Note: mimeType parameter is ignored - artifact type is inferred from filename extension
|
||||||
|
// Third parameter is title, defaults to filename
|
||||||
|
await this.updateArtifactFn(data.filename, data.content, data.filename);
|
||||||
|
// Append artifact message for session persistence
|
||||||
|
this.appendMessageFn?.({
|
||||||
|
role: "artifact",
|
||||||
|
action: "update",
|
||||||
|
filename: data.filename,
|
||||||
|
content: data.content,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
sendResponse(true);
|
||||||
|
} catch (err: any) {
|
||||||
|
sendResponse(false, undefined, err.message);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "delete": {
|
||||||
|
try {
|
||||||
|
await this.deleteArtifactFn(data.filename);
|
||||||
|
// Append artifact message for session persistence
|
||||||
|
this.appendMessageFn?.({
|
||||||
|
role: "artifact",
|
||||||
|
action: "delete",
|
||||||
|
filename: data.filename,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
sendResponse(true);
|
||||||
|
} catch (err: any) {
|
||||||
|
sendResponse(false, undefined, err.message);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
sendResponse(false, undefined, `Unknown artifact action: ${action}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error: any) {
|
||||||
|
sendResponse(false, undefined, error.message);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getDescription(): string {
|
||||||
|
return ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { ATTACHMENTS_RUNTIME_DESCRIPTION } from "../../prompts/tool-prompts.js";
|
||||||
import type { Attachment } from "../../utils/attachment-utils.js";
|
import type { Attachment } from "../../utils/attachment-utils.js";
|
||||||
import type { SandboxRuntimeProvider } from "./SandboxRuntimeProvider.js";
|
import type { SandboxRuntimeProvider } from "./SandboxRuntimeProvider.js";
|
||||||
|
|
||||||
|
|
@ -96,4 +97,8 @@ export class AttachmentsRuntimeProvider implements SandboxRuntimeProvider {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDescription(): string {
|
||||||
|
return ATTACHMENTS_RUNTIME_DESCRIPTION;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ export interface MessageConsumer {
|
||||||
* Handle a message from a sandbox.
|
* Handle a message from a sandbox.
|
||||||
* @returns true if message was consumed (stops propagation), false otherwise
|
* @returns true if message was consumed (stops propagation), false otherwise
|
||||||
*/
|
*/
|
||||||
handleMessage(message: any): boolean;
|
handleMessage(message: any): Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -111,7 +111,7 @@ export class SandboxMessageRouter {
|
||||||
private setupListener(): void {
|
private setupListener(): void {
|
||||||
if (this.messageListener) return;
|
if (this.messageListener) return;
|
||||||
|
|
||||||
this.messageListener = (e: MessageEvent) => {
|
this.messageListener = async (e: MessageEvent) => {
|
||||||
const { sandboxId } = e.data;
|
const { sandboxId } = e.data;
|
||||||
if (!sandboxId) return;
|
if (!sandboxId) return;
|
||||||
|
|
||||||
|
|
@ -129,14 +129,14 @@ export class SandboxMessageRouter {
|
||||||
// 1. Try provider handlers first (for bidirectional comm like memory)
|
// 1. Try provider handlers first (for bidirectional comm like memory)
|
||||||
for (const provider of context.providers) {
|
for (const provider of context.providers) {
|
||||||
if (provider.handleMessage) {
|
if (provider.handleMessage) {
|
||||||
const handled = provider.handleMessage(e.data, respond);
|
const handled = await provider.handleMessage(e.data, respond);
|
||||||
if (handled) return; // Stop if handled
|
if (handled) return; // Stop if handled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Broadcast to consumers (for one-way messages like console)
|
// 2. Broadcast to consumers (for one-way messages like console)
|
||||||
for (const consumer of context.consumers) {
|
for (const consumer of context.consumers) {
|
||||||
const consumed = consumer.handleMessage(e.data);
|
const consumed = await consumer.handleMessage(e.data);
|
||||||
if (consumed) break; // Stop if consumed
|
if (consumed) break; // Stop if consumed
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -26,5 +26,11 @@ export interface SandboxRuntimeProvider {
|
||||||
* @param respond - Function to send a response back to the sandbox
|
* @param respond - Function to send a response back to the sandbox
|
||||||
* @returns true if message was handled, false otherwise
|
* @returns true if message was handled, false otherwise
|
||||||
*/
|
*/
|
||||||
handleMessage?(message: any, respond: (response: any) => void): boolean;
|
handleMessage?(message: any, respond: (response: any) => void): Promise<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional documentation describing what globals/functions this provider injects.
|
||||||
|
* This will be appended to tool descriptions dynamically so the LLM knows what's available.
|
||||||
|
*/
|
||||||
|
getDescription?(): string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,8 @@ export { ModelSelector } from "./dialogs/ModelSelector.js";
|
||||||
export { PersistentStorageDialog } from "./dialogs/PersistentStorageDialog.js";
|
export { PersistentStorageDialog } from "./dialogs/PersistentStorageDialog.js";
|
||||||
export { SessionListDialog } from "./dialogs/SessionListDialog.js";
|
export { SessionListDialog } from "./dialogs/SessionListDialog.js";
|
||||||
export { ApiKeysTab, ProxyTab, SettingsDialog, SettingsTab } from "./dialogs/SettingsDialog.js";
|
export { ApiKeysTab, ProxyTab, SettingsDialog, SettingsTab } from "./dialogs/SettingsDialog.js";
|
||||||
|
// Prompts
|
||||||
|
export { ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION } from "./prompts/tool-prompts.js";
|
||||||
// Storage
|
// Storage
|
||||||
export { AppStorage, getAppStorage, setAppStorage } from "./storage/app-storage.js";
|
export { AppStorage, getAppStorage, setAppStorage } from "./storage/app-storage.js";
|
||||||
export { IndexedDBStorageBackend } from "./storage/backends/indexeddb-storage-backend.js";
|
export { IndexedDBStorageBackend } from "./storage/backends/indexeddb-storage-backend.js";
|
||||||
|
|
|
||||||
251
packages/web-ui/src/prompts/tool-prompts.ts
Normal file
251
packages/web-ui/src/prompts/tool-prompts.ts
Normal file
|
|
@ -0,0 +1,251 @@
|
||||||
|
/**
|
||||||
|
* Centralized tool prompts/descriptions.
|
||||||
|
* Each prompt is either a string constant or a template function.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// JavaScript REPL Tool
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const JAVASCRIPT_REPL_BASE_DESCRIPTION = `Execute JavaScript code in a sandboxed browser environment with full modern browser capabilities.
|
||||||
|
|
||||||
|
Environment: Modern browser with ALL Web APIs available:
|
||||||
|
- ES2023+ JavaScript (async/await, optional chaining, nullish coalescing, etc.)
|
||||||
|
- DOM APIs (document, window, Canvas, WebGL, etc.)
|
||||||
|
- Fetch API for HTTP requests
|
||||||
|
|
||||||
|
Loading external libraries via dynamic imports (use esm.run):
|
||||||
|
- XLSX (Excel files): const XLSX = await import('https://esm.run/xlsx');
|
||||||
|
- Papa Parse (CSV): const Papa = (await import('https://esm.run/papaparse')).default;
|
||||||
|
- Lodash: const _ = await import('https://esm.run/lodash-es');
|
||||||
|
- D3.js: const d3 = await import('https://esm.run/d3');
|
||||||
|
- Chart.js: const Chart = (await import('https://esm.run/chart.js/auto')).default;
|
||||||
|
- Three.js: const THREE = await import('https://esm.run/three');
|
||||||
|
- Any npm package: await import('https://esm.run/package-name')
|
||||||
|
|
||||||
|
IMPORTANT for graphics/canvas:
|
||||||
|
- Use fixed dimensions like 400x400 or 800x600, NOT window.innerWidth/Height
|
||||||
|
- For Three.js: renderer.setSize(400, 400) and camera aspect ratio of 1
|
||||||
|
- For Chart.js: Set options: { responsive: false, animation: false } to ensure immediate rendering
|
||||||
|
- Web Storage (localStorage, sessionStorage, IndexedDB)
|
||||||
|
- Web Workers, WebAssembly, WebSockets
|
||||||
|
- Media APIs (Audio, Video, WebRTC)
|
||||||
|
- File APIs (Blob, FileReader, etc.)
|
||||||
|
- Crypto API for cryptography
|
||||||
|
- And much more - anything a modern browser supports!
|
||||||
|
|
||||||
|
Output:
|
||||||
|
- console.log() - All output is captured as text`;
|
||||||
|
|
||||||
|
export const JAVASCRIPT_REPL_CHART_EXAMPLE = `
|
||||||
|
- Chart.js example:
|
||||||
|
const Chart = (await import('https://esm.run/chart.js/auto')).default;
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = 400; canvas.height = 300;
|
||||||
|
document.body.appendChild(canvas);
|
||||||
|
new Chart(canvas, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: ['Jan', 'Feb', 'Mar', 'Apr'],
|
||||||
|
datasets: [{ label: 'Sales', data: [10, 20, 15, 25], borderColor: 'blue' }]
|
||||||
|
},
|
||||||
|
options: { responsive: false, animation: false }
|
||||||
|
});
|
||||||
|
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
|
||||||
|
await returnFile('chart.png', blob, 'image/png');`;
|
||||||
|
|
||||||
|
export const JAVASCRIPT_REPL_FOOTER = `
|
||||||
|
|
||||||
|
- All standard browser globals (window, document, fetch, etc.)`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build complete JavaScript REPL description with optional provider docs.
|
||||||
|
*/
|
||||||
|
export function buildJavaScriptReplDescription(providerDocs?: string): string {
|
||||||
|
return (
|
||||||
|
JAVASCRIPT_REPL_BASE_DESCRIPTION +
|
||||||
|
(providerDocs ? "\n" + providerDocs + JAVASCRIPT_REPL_CHART_EXAMPLE : "") +
|
||||||
|
JAVASCRIPT_REPL_FOOTER
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Artifacts Tool
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const ARTIFACTS_BASE_DESCRIPTION = `Creates and manages file artifacts. Each artifact is a file with a filename and content.
|
||||||
|
|
||||||
|
IMPORTANT: Always prefer updating existing files over creating new ones. Check available files first.
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
1. create: Create a new file
|
||||||
|
- filename: Name with extension (required, e.g., 'index.html', 'script.js', 'README.md')
|
||||||
|
- title: Display name for the tab (optional, defaults to filename)
|
||||||
|
- content: File content (required)
|
||||||
|
|
||||||
|
2. update: Update part of an existing file
|
||||||
|
- filename: File to update (required)
|
||||||
|
- old_str: Exact string to replace (required)
|
||||||
|
- new_str: Replacement string (required)
|
||||||
|
|
||||||
|
3. rewrite: Completely replace a file's content
|
||||||
|
- filename: File to rewrite (required)
|
||||||
|
- content: New content (required)
|
||||||
|
- title: Optionally update display title
|
||||||
|
|
||||||
|
4. get: Retrieve the full content of a file
|
||||||
|
- filename: File to retrieve (required)
|
||||||
|
- Returns the complete file content
|
||||||
|
|
||||||
|
5. delete: Delete a file
|
||||||
|
- filename: File to delete (required)
|
||||||
|
|
||||||
|
6. logs: Get console logs and errors (HTML files only)
|
||||||
|
- filename: HTML file to get logs for (required)
|
||||||
|
- Returns all console output and runtime errors`;
|
||||||
|
|
||||||
|
export const ARTIFACTS_RUNTIME_EXAMPLE = `- Example HTML artifact that processes a CSV attachment:
|
||||||
|
<script>
|
||||||
|
// List available files
|
||||||
|
const files = listFiles();
|
||||||
|
console.log('Available files:', files);
|
||||||
|
|
||||||
|
// Find CSV file
|
||||||
|
const csvFile = files.find(f => f.mimeType === 'text/csv');
|
||||||
|
if (csvFile) {
|
||||||
|
const csvContent = readTextFile(csvFile.id);
|
||||||
|
// Process CSV data...
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display image
|
||||||
|
const imageFile = files.find(f => f.mimeType.startsWith('image/'));
|
||||||
|
if (imageFile) {
|
||||||
|
const bytes = readBinaryFile(imageFile.id);
|
||||||
|
const blob = new Blob([bytes], {type: imageFile.mimeType});
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
document.body.innerHTML = '<img src="' + url + '">';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ARTIFACTS_HTML_SECTION = `
|
||||||
|
For text/html artifacts:
|
||||||
|
- Must be a single self-contained file
|
||||||
|
- External scripts: Use CDNs like https://esm.sh, https://unpkg.com, or https://cdnjs.cloudflare.com
|
||||||
|
- Preferred: Use https://esm.sh for npm packages (e.g., https://esm.sh/three for Three.js)
|
||||||
|
- For ES modules, use: <script type="module">import * as THREE from 'https://esm.sh/three';</script>
|
||||||
|
- For Three.js specifically: import from 'https://esm.sh/three' or 'https://esm.sh/three@0.160.0'
|
||||||
|
- 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
|
||||||
|
|
||||||
|
For application/vnd.ant.code artifacts:
|
||||||
|
- Include the language parameter for syntax highlighting
|
||||||
|
- Supports all major programming languages
|
||||||
|
|
||||||
|
For text/markdown:
|
||||||
|
- Standard markdown syntax
|
||||||
|
- Will be rendered with full formatting
|
||||||
|
- Can include base64 images using markdown syntax
|
||||||
|
|
||||||
|
For image/svg+xml:
|
||||||
|
- Complete SVG markup
|
||||||
|
- Will be rendered inline
|
||||||
|
- 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`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build complete artifacts description with optional provider docs.
|
||||||
|
*/
|
||||||
|
export function buildArtifactsDescription(providerDocs?: string): string {
|
||||||
|
const runtimeSection = providerDocs
|
||||||
|
? `
|
||||||
|
|
||||||
|
For text/html artifacts with runtime capabilities:${providerDocs}
|
||||||
|
${ARTIFACTS_RUNTIME_EXAMPLE}
|
||||||
|
`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return ARTIFACTS_BASE_DESCRIPTION + runtimeSection + ARTIFACTS_HTML_SECTION;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Artifacts Runtime Provider
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const ARTIFACTS_RUNTIME_PROVIDER_DESCRIPTION = `
|
||||||
|
Artifact Management (persistent session files you can access/modify programmatically):
|
||||||
|
- await hasArtifact(filename) - Check if artifact exists, returns boolean
|
||||||
|
* Example: if (await hasArtifact('data.json')) { ... }
|
||||||
|
- await getArtifact(filename) - Read artifact content, returns string or object
|
||||||
|
* Auto-parses .json files to objects, otherwise returns raw string content
|
||||||
|
* Example: const data = await getArtifact('data.json'); // Returns parsed object
|
||||||
|
* Example: const markdown = await getArtifact('notes.md'); // Returns string
|
||||||
|
- await createArtifact(filename, content) - Create new persistent artifact
|
||||||
|
* Auto-stringifies objects for .json files
|
||||||
|
* Example: await createArtifact('data.json', {items: []}) // Auto-stringifies
|
||||||
|
* Example: await createArtifact('research-notes.md', '# Research Notes\\n', 'text/markdown')
|
||||||
|
- await updateArtifact(filename, content) - Completely replace artifact content
|
||||||
|
* Auto-stringifies objects for .json files
|
||||||
|
* Full content replacement (not diff-based)
|
||||||
|
* Example: const data = await getArtifact('data.json'); data.items.push(newItem); await updateArtifact('data.json', data);
|
||||||
|
* Example: await updateArtifact('research-notes.md', updatedMarkdown, 'text/markdown')
|
||||||
|
- await deleteArtifact(filename) - Delete an artifact
|
||||||
|
* Example: await deleteArtifact('old-notes.md')
|
||||||
|
|
||||||
|
Powerful pattern for evolving data:
|
||||||
|
const data = await hasArtifact('data.json') ? await getArtifact('data.json') : {items: []};
|
||||||
|
data.items.push(newScrapedItem);
|
||||||
|
await (await hasArtifact('data.json') ? updateArtifact : createArtifact)('data.json', data);
|
||||||
|
|
||||||
|
Binary data must be converted to a base64 string before passing to createArtifact or updateArtifact.
|
||||||
|
Example:
|
||||||
|
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
|
||||||
|
const arrayBuffer = await blob.arrayBuffer();
|
||||||
|
const base64 = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
|
||||||
|
await createArtifact('image.png', base64);
|
||||||
|
`;
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Attachments Runtime Provider
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
export const ATTACHMENTS_RUNTIME_DESCRIPTION = `
|
||||||
|
Global variables:
|
||||||
|
- attachments[] - Array of attachment objects from user messages
|
||||||
|
* Properties:
|
||||||
|
- id: string (unique identifier)
|
||||||
|
- fileName: string (e.g., "data.xlsx")
|
||||||
|
- mimeType: string (e.g., "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||||
|
- size: number (bytes)
|
||||||
|
* Helper functions:
|
||||||
|
- listFiles() - Returns array of {id, fileName, mimeType, size} for all attachments
|
||||||
|
- readTextFile(attachmentId) - Returns text content of attachment (for CSV, JSON, text files)
|
||||||
|
- readBinaryFile(attachmentId) - Returns Uint8Array of binary data (for images, Excel, etc.)
|
||||||
|
* Examples:
|
||||||
|
- const files = listFiles();
|
||||||
|
- const csvContent = readTextFile(files[0].id); // Read CSV as text
|
||||||
|
- const xlsxBytes = readBinaryFile(files[0].id); // Read Excel as binary
|
||||||
|
- await returnFile(filename, content, mimeType?) - Create downloadable files (async function!)
|
||||||
|
* Always use await with returnFile
|
||||||
|
* REQUIRED: For Blob/Uint8Array binary content, you MUST supply a proper MIME type (e.g., "image/png").
|
||||||
|
If omitted, the REPL throws an Error with stack trace pointing to the offending line.
|
||||||
|
* Strings without a MIME default to text/plain.
|
||||||
|
* Objects are auto-JSON stringified and default to application/json unless a MIME is provided.
|
||||||
|
* Canvas images: Use toBlob() with await Promise wrapper
|
||||||
|
* Examples:
|
||||||
|
- await returnFile('data.txt', 'Hello World', 'text/plain')
|
||||||
|
- await returnFile('data.json', {key: 'value'}, 'application/json')
|
||||||
|
- await returnFile('data.csv', 'name,age\\nJohn,30', 'text/csv')`;
|
||||||
|
|
@ -2,7 +2,6 @@ import { LitElement, type TemplateResult } from "lit";
|
||||||
|
|
||||||
export abstract class ArtifactElement extends LitElement {
|
export abstract class ArtifactElement extends LitElement {
|
||||||
public filename = "";
|
public filename = "";
|
||||||
public displayTitle = "";
|
|
||||||
|
|
||||||
protected override createRenderRoot(): HTMLElement | DocumentFragment {
|
protected override createRenderRoot(): HTMLElement | DocumentFragment {
|
||||||
return this; // light DOM for shared styles
|
return this; // light DOM for shared styles
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ import "./Console.js";
|
||||||
@customElement("html-artifact")
|
@customElement("html-artifact")
|
||||||
export class HtmlArtifact extends ArtifactElement {
|
export class HtmlArtifact extends ArtifactElement {
|
||||||
@property() override filename = "";
|
@property() override filename = "";
|
||||||
@property({ attribute: false }) override displayTitle = "";
|
|
||||||
@property({ attribute: false }) runtimeProviders: SandboxRuntimeProvider[] = [];
|
@property({ attribute: false }) runtimeProviders: SandboxRuntimeProvider[] = [];
|
||||||
@property({ attribute: false }) sandboxUrlProvider?: () => string;
|
@property({ attribute: false }) sandboxUrlProvider?: () => string;
|
||||||
|
|
||||||
|
|
@ -87,7 +86,7 @@ export class HtmlArtifact extends ArtifactElement {
|
||||||
|
|
||||||
// Create consumer for console messages
|
// Create consumer for console messages
|
||||||
const consumer: MessageConsumer = {
|
const consumer: MessageConsumer = {
|
||||||
handleMessage: (message: any): boolean => {
|
handleMessage: async (message: any): Promise<boolean> => {
|
||||||
if (message.type === "console") {
|
if (message.type === "console") {
|
||||||
// Create new array reference for Lit reactivity
|
// Create new array reference for Lit reactivity
|
||||||
this.logs = [
|
this.logs = [
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import { ArtifactElement } from "./ArtifactElement.js";
|
||||||
@customElement("markdown-artifact")
|
@customElement("markdown-artifact")
|
||||||
export class MarkdownArtifact extends ArtifactElement {
|
export class MarkdownArtifact extends ArtifactElement {
|
||||||
@property() override filename = "";
|
@property() override filename = "";
|
||||||
@property({ attribute: false }) override displayTitle = "";
|
|
||||||
|
|
||||||
private _content = "";
|
private _content = "";
|
||||||
override get content(): string {
|
override get content(): string {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import { ArtifactElement } from "./ArtifactElement.js";
|
||||||
@customElement("svg-artifact")
|
@customElement("svg-artifact")
|
||||||
export class SvgArtifact extends ArtifactElement {
|
export class SvgArtifact extends ArtifactElement {
|
||||||
@property() override filename = "";
|
@property() override filename = "";
|
||||||
@property({ attribute: false }) override displayTitle = "";
|
|
||||||
|
|
||||||
private _content = "";
|
private _content = "";
|
||||||
override get content(): string {
|
override get content(): string {
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,6 @@ const CODE_EXTENSIONS = [
|
||||||
@customElement("text-artifact")
|
@customElement("text-artifact")
|
||||||
export class TextArtifact extends ArtifactElement {
|
export class TextArtifact extends ArtifactElement {
|
||||||
@property() override filename = "";
|
@property() override filename = "";
|
||||||
@property({ attribute: false }) override displayTitle = "";
|
|
||||||
|
|
||||||
private _content = "";
|
private _content = "";
|
||||||
override get content(): string {
|
override get content(): string {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,9 @@ import { html, LitElement, type TemplateResult } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators.js";
|
import { customElement, property, state } from "lit/decorators.js";
|
||||||
import { createRef, type Ref, ref } from "lit/directives/ref.js";
|
import { createRef, type Ref, ref } from "lit/directives/ref.js";
|
||||||
import { X } from "lucide";
|
import { X } from "lucide";
|
||||||
|
import type { ArtifactMessage } from "../../components/Messages.js";
|
||||||
import type { SandboxRuntimeProvider } from "../../components/sandbox/SandboxRuntimeProvider.js";
|
import type { SandboxRuntimeProvider } from "../../components/sandbox/SandboxRuntimeProvider.js";
|
||||||
|
import { buildArtifactsDescription } from "../../prompts/tool-prompts.js";
|
||||||
import { i18n } from "../../utils/i18n.js";
|
import { i18n } from "../../utils/i18n.js";
|
||||||
import type { ArtifactElement } from "./ArtifactElement.js";
|
import type { ArtifactElement } from "./ArtifactElement.js";
|
||||||
import { HtmlArtifact } from "./HtmlArtifact.js";
|
import { HtmlArtifact } from "./HtmlArtifact.js";
|
||||||
|
|
@ -17,7 +19,6 @@ import { TextArtifact } from "./TextArtifact.js";
|
||||||
// Simple artifact model
|
// Simple artifact model
|
||||||
export interface Artifact {
|
export interface Artifact {
|
||||||
filename: string;
|
filename: string;
|
||||||
title: string;
|
|
||||||
content: string;
|
content: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
|
@ -29,7 +30,6 @@ const artifactsParamsSchema = Type.Object({
|
||||||
description: "The operation to perform",
|
description: "The operation to perform",
|
||||||
}),
|
}),
|
||||||
filename: Type.String({ description: "Filename including extension (e.g., 'index.html', 'script.js')" }),
|
filename: Type.String({ description: "Filename including extension (e.g., 'index.html', 'script.js')" }),
|
||||||
title: Type.Optional(Type.String({ description: "Display title for the tab (defaults to filename)" })),
|
|
||||||
content: Type.Optional(Type.String({ description: "File content" })),
|
content: Type.Optional(Type.String({ description: "File content" })),
|
||||||
old_str: Type.Optional(Type.String({ description: "String to replace (for update command)" })),
|
old_str: Type.Optional(Type.String({ description: "String to replace (for update command)" })),
|
||||||
new_str: Type.Optional(Type.String({ description: "Replacement string (for update command)" })),
|
new_str: Type.Optional(Type.String({ description: "Replacement string (for update command)" })),
|
||||||
|
|
@ -101,7 +101,7 @@ export class ArtifactsPanel extends LitElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get or create artifact element
|
// Get or create artifact element
|
||||||
private getOrCreateArtifactElement(filename: string, content: string, title: string): ArtifactElement {
|
private getOrCreateArtifactElement(filename: string, content: string): ArtifactElement {
|
||||||
let element = this.artifactElements.get(filename);
|
let element = this.artifactElements.get(filename);
|
||||||
|
|
||||||
if (!element) {
|
if (!element) {
|
||||||
|
|
@ -121,7 +121,6 @@ export class ArtifactsPanel extends LitElement {
|
||||||
element = new TextArtifact();
|
element = new TextArtifact();
|
||||||
}
|
}
|
||||||
element.filename = filename;
|
element.filename = filename;
|
||||||
element.displayTitle = title;
|
|
||||||
element.content = content;
|
element.content = content;
|
||||||
element.style.display = "none";
|
element.style.display = "none";
|
||||||
element.style.height = "100%";
|
element.style.height = "100%";
|
||||||
|
|
@ -143,7 +142,6 @@ export class ArtifactsPanel extends LitElement {
|
||||||
} else {
|
} else {
|
||||||
// Just update content
|
// Just update content
|
||||||
element.content = content;
|
element.content = content;
|
||||||
element.displayTitle = title;
|
|
||||||
if (element instanceof HtmlArtifact) {
|
if (element instanceof HtmlArtifact) {
|
||||||
const runtimeProviders = this.runtimeProvidersFactory?.() || [];
|
const runtimeProviders = this.runtimeProvidersFactory?.() || [];
|
||||||
element.runtimeProviders = runtimeProviders;
|
element.runtimeProviders = runtimeProviders;
|
||||||
|
|
@ -179,106 +177,20 @@ export class ArtifactsPanel extends LitElement {
|
||||||
|
|
||||||
// Build the AgentTool (no details payload; return only output strings)
|
// Build the AgentTool (no details payload; return only output strings)
|
||||||
public get tool(): AgentTool<typeof artifactsParamsSchema, undefined> {
|
public get tool(): AgentTool<typeof artifactsParamsSchema, undefined> {
|
||||||
|
const self = this;
|
||||||
return {
|
return {
|
||||||
label: "Artifacts",
|
label: "Artifacts",
|
||||||
name: "artifacts",
|
name: "artifacts",
|
||||||
description: `Creates and manages file artifacts. Each artifact is a file with a filename and content.
|
get description() {
|
||||||
|
// Get dynamic provider descriptions
|
||||||
|
const providers = self.runtimeProvidersFactory?.() || [];
|
||||||
|
const providerDocs = providers
|
||||||
|
.map((p) => p.getDescription?.())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
IMPORTANT: Always prefer updating existing files over creating new ones. Check available files first.
|
return buildArtifactsDescription(providerDocs || undefined);
|
||||||
|
},
|
||||||
Commands:
|
|
||||||
1. create: Create a new file
|
|
||||||
- filename: Name with extension (required, e.g., 'index.html', 'script.js', 'README.md')
|
|
||||||
- title: Display name for the tab (optional, defaults to filename)
|
|
||||||
- content: File content (required)
|
|
||||||
|
|
||||||
2. update: Update part of an existing file
|
|
||||||
- filename: File to update (required)
|
|
||||||
- old_str: Exact string to replace (required)
|
|
||||||
- new_str: Replacement string (required)
|
|
||||||
|
|
||||||
3. rewrite: Completely replace a file's content
|
|
||||||
- filename: File to rewrite (required)
|
|
||||||
- content: New content (required)
|
|
||||||
- title: Optionally update display title
|
|
||||||
|
|
||||||
4. get: Retrieve the full content of a file
|
|
||||||
- filename: File to retrieve (required)
|
|
||||||
- Returns the complete file content
|
|
||||||
|
|
||||||
5. delete: Delete a file
|
|
||||||
- filename: File to delete (required)
|
|
||||||
|
|
||||||
6. logs: Get console logs and errors (HTML files only)
|
|
||||||
- filename: HTML file to get logs for (required)
|
|
||||||
- Returns all console output and runtime errors
|
|
||||||
|
|
||||||
For text/html artifacts with attachments:
|
|
||||||
- HTML artifacts automatically have access to user attachments via JavaScript
|
|
||||||
- Available global functions in HTML artifacts:
|
|
||||||
* listFiles() - Returns array of {id, fileName, mimeType, size} for all attachments
|
|
||||||
* readTextFile(attachmentId) - Returns text content of attachment (for CSV, JSON, text files)
|
|
||||||
* readBinaryFile(attachmentId) - Returns Uint8Array of binary data (for images, Excel, etc.)
|
|
||||||
- Example HTML artifact that processes a CSV attachment:
|
|
||||||
<script>
|
|
||||||
// List available files
|
|
||||||
const files = listFiles();
|
|
||||||
console.log('Available files:', files);
|
|
||||||
|
|
||||||
// Find CSV file
|
|
||||||
const csvFile = files.find(f => f.mimeType === 'text/csv');
|
|
||||||
if (csvFile) {
|
|
||||||
const csvContent = readTextFile(csvFile.id);
|
|
||||||
// Process CSV data...
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display image
|
|
||||||
const imageFile = files.find(f => f.mimeType.startsWith('image/'));
|
|
||||||
if (imageFile) {
|
|
||||||
const bytes = readBinaryFile(imageFile.id);
|
|
||||||
const blob = new Blob([bytes], {type: imageFile.mimeType});
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
document.body.innerHTML = '<img src="' + url + '">';
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
For text/html artifacts:
|
|
||||||
- Must be a single self-contained file
|
|
||||||
- External scripts: Use CDNs like https://esm.sh, https://unpkg.com, or https://cdnjs.cloudflare.com
|
|
||||||
- Preferred: Use https://esm.sh for npm packages (e.g., https://esm.sh/three for Three.js)
|
|
||||||
- For ES modules, use: <script type="module">import * as THREE from 'https://esm.sh/three';</script>
|
|
||||||
- For Three.js specifically: import from 'https://esm.sh/three' or 'https://esm.sh/three@0.160.0'
|
|
||||||
- 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
|
|
||||||
|
|
||||||
For application/vnd.ant.code artifacts:
|
|
||||||
- Include the language parameter for syntax highlighting
|
|
||||||
- Supports all major programming languages
|
|
||||||
|
|
||||||
For text/markdown:
|
|
||||||
- Standard markdown syntax
|
|
||||||
- Will be rendered with full formatting
|
|
||||||
- Can include base64 images using markdown syntax
|
|
||||||
|
|
||||||
For image/svg+xml:
|
|
||||||
- Complete SVG markup
|
|
||||||
- Will be rendered inline
|
|
||||||
- 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,
|
parameters: artifactsParamsSchema,
|
||||||
// Execute mutates our local store and returns a plain output
|
// Execute mutates our local store and returns a plain output
|
||||||
execute: async (_toolCallId: string, args: Static<typeof artifactsParamsSchema>, _signal?: AbortSignal) => {
|
execute: async (_toolCallId: string, args: Static<typeof artifactsParamsSchema>, _signal?: AbortSignal) => {
|
||||||
|
|
@ -289,7 +201,9 @@ CRITICAL REMINDER FOR ALL ARTIFACTS:
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-apply artifacts by scanning a message list (optional utility)
|
// Re-apply artifacts by scanning a message list (optional utility)
|
||||||
public async reconstructFromMessages(messages: Array<Message | { role: "aborted" }>): Promise<void> {
|
public async reconstructFromMessages(
|
||||||
|
messages: Array<Message | { role: "aborted" } | { role: "artifact" }>,
|
||||||
|
): Promise<void> {
|
||||||
const toolCalls = new Map<string, ToolCall>();
|
const toolCalls = new Map<string, ToolCall>();
|
||||||
const artifactToolName = "artifacts";
|
const artifactToolName = "artifacts";
|
||||||
|
|
||||||
|
|
@ -307,7 +221,34 @@ CRITICAL REMINDER FOR ALL ARTIFACTS:
|
||||||
// 2) Build an ordered list of successful artifact operations
|
// 2) Build an ordered list of successful artifact operations
|
||||||
const operations: Array<ArtifactsParams> = [];
|
const operations: Array<ArtifactsParams> = [];
|
||||||
for (const m of messages) {
|
for (const m of messages) {
|
||||||
if ((m as any).role === "toolResult" && (m as any).toolName === artifactToolName && !(m as any).isError) {
|
// Handle artifact messages (from programmatic operations like browser_javascript)
|
||||||
|
if ((m as any).role === "artifact") {
|
||||||
|
const artifactMsg = m as ArtifactMessage;
|
||||||
|
switch (artifactMsg.action) {
|
||||||
|
case "create":
|
||||||
|
operations.push({
|
||||||
|
command: "create",
|
||||||
|
filename: artifactMsg.filename,
|
||||||
|
content: artifactMsg.content,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "update":
|
||||||
|
operations.push({
|
||||||
|
command: "rewrite",
|
||||||
|
filename: artifactMsg.filename,
|
||||||
|
content: artifactMsg.content,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "delete":
|
||||||
|
operations.push({
|
||||||
|
command: "delete",
|
||||||
|
filename: artifactMsg.filename,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Handle tool result messages (from artifacts tool calls)
|
||||||
|
else if ((m as any).role === "toolResult" && (m as any).toolName === artifactToolName && !(m as any).isError) {
|
||||||
const toolCallId = (m as any).toolCallId as string;
|
const toolCallId = (m as any).toolCallId as string;
|
||||||
const call = toolCalls.get(toolCallId);
|
const call = toolCalls.get(toolCallId);
|
||||||
if (!call) continue;
|
if (!call) continue;
|
||||||
|
|
@ -318,30 +259,27 @@ CRITICAL REMINDER FOR ALL ARTIFACTS:
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Compute final state per filename by simulating operations in-memory
|
// 3) Compute final state per filename by simulating operations in-memory
|
||||||
type FinalArtifact = { title: string; content: string };
|
const finalArtifacts = new Map<string, string>();
|
||||||
const finalArtifacts = new Map<string, FinalArtifact>();
|
|
||||||
for (const op of operations) {
|
for (const op of operations) {
|
||||||
const filename = op.filename;
|
const filename = op.filename;
|
||||||
switch (op.command) {
|
switch (op.command) {
|
||||||
case "create": {
|
case "create": {
|
||||||
if (op.content) {
|
if (op.content) {
|
||||||
finalArtifacts.set(filename, { title: op.title || filename, content: op.content });
|
finalArtifacts.set(filename, op.content);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "rewrite": {
|
case "rewrite": {
|
||||||
if (op.content) {
|
if (op.content) {
|
||||||
// If file didn't exist earlier but rewrite succeeded, treat as fresh content
|
finalArtifacts.set(filename, op.content);
|
||||||
const existing = finalArtifacts.get(filename);
|
|
||||||
finalArtifacts.set(filename, { title: op.title || existing?.title || filename, content: op.content });
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "update": {
|
case "update": {
|
||||||
const existing = finalArtifacts.get(filename);
|
let existing = finalArtifacts.get(filename);
|
||||||
if (!existing) break; // skip invalid update (shouldn't happen for successful results)
|
if (!existing) break; // skip invalid update (shouldn't happen for successful results)
|
||||||
if (op.old_str !== undefined && op.new_str !== undefined) {
|
if (op.old_str !== undefined && op.new_str !== undefined) {
|
||||||
existing.content = existing.content.replace(op.old_str, op.new_str);
|
existing = existing.replace(op.old_str, op.new_str);
|
||||||
finalArtifacts.set(filename, existing);
|
finalArtifacts.set(filename, existing);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -367,8 +305,8 @@ CRITICAL REMINDER FOR ALL ARTIFACTS:
|
||||||
this._artifacts = new Map(this._artifacts);
|
this._artifacts = new Map(this._artifacts);
|
||||||
|
|
||||||
// 5) Create artifacts in a single pass without waiting for iframe execution or tab switching
|
// 5) Create artifacts in a single pass without waiting for iframe execution or tab switching
|
||||||
for (const [filename, { title, content }] of finalArtifacts.entries()) {
|
for (const [filename, content] of finalArtifacts.entries()) {
|
||||||
const createParams: ArtifactsParams = { command: "create", filename, title, content } as const;
|
const createParams: ArtifactsParams = { command: "create", filename, content } as const;
|
||||||
try {
|
try {
|
||||||
await this.createArtifact(createParams, { skipWait: true, silent: true });
|
await this.createArtifact(createParams, { skipWait: true, silent: true });
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -436,10 +374,8 @@ CRITICAL REMINDER FOR ALL ARTIFACTS:
|
||||||
return `Error: File ${params.filename} already exists`;
|
return `Error: File ${params.filename} already exists`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = params.title || params.filename;
|
|
||||||
const artifact: Artifact = {
|
const artifact: Artifact = {
|
||||||
filename: params.filename,
|
filename: params.filename,
|
||||||
title: title,
|
|
||||||
content: params.content,
|
content: params.content,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
|
|
@ -448,7 +384,7 @@ CRITICAL REMINDER FOR ALL ARTIFACTS:
|
||||||
this._artifacts = new Map(this._artifacts);
|
this._artifacts = new Map(this._artifacts);
|
||||||
|
|
||||||
// Create or update element
|
// Create or update element
|
||||||
this.getOrCreateArtifactElement(params.filename, params.content, title);
|
this.getOrCreateArtifactElement(params.filename, params.content);
|
||||||
if (!options.silent) {
|
if (!options.silent) {
|
||||||
this.showArtifact(params.filename);
|
this.showArtifact(params.filename);
|
||||||
this.onArtifactsChange?.();
|
this.onArtifactsChange?.();
|
||||||
|
|
@ -487,7 +423,7 @@ CRITICAL REMINDER FOR ALL ARTIFACTS:
|
||||||
this._artifacts.set(params.filename, artifact);
|
this._artifacts.set(params.filename, artifact);
|
||||||
|
|
||||||
// Update element
|
// Update element
|
||||||
this.getOrCreateArtifactElement(params.filename, artifact.content, artifact.title);
|
this.getOrCreateArtifactElement(params.filename, artifact.content);
|
||||||
if (!options.silent) {
|
if (!options.silent) {
|
||||||
this.onArtifactsChange?.();
|
this.onArtifactsChange?.();
|
||||||
this.requestUpdate();
|
this.requestUpdate();
|
||||||
|
|
@ -521,12 +457,11 @@ CRITICAL REMINDER FOR ALL ARTIFACTS:
|
||||||
}
|
}
|
||||||
|
|
||||||
artifact.content = params.content;
|
artifact.content = params.content;
|
||||||
if (params.title) artifact.title = params.title;
|
|
||||||
artifact.updatedAt = new Date();
|
artifact.updatedAt = new Date();
|
||||||
this._artifacts.set(params.filename, artifact);
|
this._artifacts.set(params.filename, artifact);
|
||||||
|
|
||||||
// Update element
|
// Update element
|
||||||
this.getOrCreateArtifactElement(params.filename, artifact.content, artifact.title);
|
this.getOrCreateArtifactElement(params.filename, artifact.content);
|
||||||
if (!options.silent) {
|
if (!options.silent) {
|
||||||
this.onArtifactsChange?.();
|
this.onArtifactsChange?.();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { createRef, ref } from "lit/directives/ref.js";
|
||||||
import { Code } from "lucide";
|
import { Code } from "lucide";
|
||||||
import { type SandboxFile, SandboxIframe, type SandboxResult } from "../components/SandboxedIframe.js";
|
import { type SandboxFile, SandboxIframe, type SandboxResult } from "../components/SandboxedIframe.js";
|
||||||
import type { SandboxRuntimeProvider } from "../components/sandbox/SandboxRuntimeProvider.js";
|
import type { SandboxRuntimeProvider } from "../components/sandbox/SandboxRuntimeProvider.js";
|
||||||
|
import { buildJavaScriptReplDescription } from "../prompts/tool-prompts.js";
|
||||||
import type { Attachment } from "../utils/attachment-utils.js";
|
import type { Attachment } from "../utils/attachment-utils.js";
|
||||||
import { registerToolRenderer, renderCollapsibleHeader, renderHeader } from "./renderer-registry.js";
|
import { registerToolRenderer, renderCollapsibleHeader, renderHeader } from "./renderer-registry.js";
|
||||||
import type { ToolRenderer } from "./types.js";
|
import type { ToolRenderer } from "./types.js";
|
||||||
|
|
@ -125,78 +126,16 @@ export function createJavaScriptReplTool(): AgentTool<typeof javascriptReplSchem
|
||||||
name: "javascript_repl",
|
name: "javascript_repl",
|
||||||
runtimeProvidersFactory: () => [], // default to empty array
|
runtimeProvidersFactory: () => [], // default to empty array
|
||||||
sandboxUrlProvider: undefined, // optional, for browser extensions
|
sandboxUrlProvider: undefined, // optional, for browser extensions
|
||||||
description: `Execute JavaScript code in a sandboxed browser environment with full modern browser capabilities.
|
get description() {
|
||||||
|
// Get dynamic provider descriptions
|
||||||
|
const providers = this.runtimeProvidersFactory?.() || [];
|
||||||
|
const providerDocs = providers
|
||||||
|
.map((p) => p.getDescription?.())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
Environment: Modern browser with ALL Web APIs available:
|
return buildJavaScriptReplDescription(providerDocs || undefined);
|
||||||
- ES2023+ JavaScript (async/await, optional chaining, nullish coalescing, etc.)
|
|
||||||
- DOM APIs (document, window, Canvas, WebGL, etc.)
|
|
||||||
- Fetch API for HTTP requests
|
|
||||||
|
|
||||||
Loading external libraries via dynamic imports (use esm.run):
|
|
||||||
- XLSX (Excel files): const XLSX = await import('https://esm.run/xlsx');
|
|
||||||
- Papa Parse (CSV): const Papa = (await import('https://esm.run/papaparse')).default;
|
|
||||||
- Lodash: const _ = await import('https://esm.run/lodash-es');
|
|
||||||
- D3.js: const d3 = await import('https://esm.run/d3');
|
|
||||||
- Chart.js: const Chart = (await import('https://esm.run/chart.js/auto')).default;
|
|
||||||
- Three.js: const THREE = await import('https://esm.run/three');
|
|
||||||
- Any npm package: await import('https://esm.run/package-name')
|
|
||||||
|
|
||||||
IMPORTANT for graphics/canvas:
|
|
||||||
- Use fixed dimensions like 400x400 or 800x600, NOT window.innerWidth/Height
|
|
||||||
- For Three.js: renderer.setSize(400, 400) and camera aspect ratio of 1
|
|
||||||
- For Chart.js: Set options: { responsive: false, animation: false } to ensure immediate rendering
|
|
||||||
- Web Storage (localStorage, sessionStorage, IndexedDB)
|
|
||||||
- Web Workers, WebAssembly, WebSockets
|
|
||||||
- Media APIs (Audio, Video, WebRTC)
|
|
||||||
- File APIs (Blob, FileReader, etc.)
|
|
||||||
- Crypto API for cryptography
|
|
||||||
- And much more - anything a modern browser supports!
|
|
||||||
|
|
||||||
Output:
|
|
||||||
- console.log() - All output is captured as text
|
|
||||||
- await returnFile(filename, content, mimeType?) - Create downloadable files (async function!)
|
|
||||||
* Always use await with returnFile
|
|
||||||
* REQUIRED: For Blob/Uint8Array binary content, you MUST supply a proper MIME type (e.g., "image/png").
|
|
||||||
If omitted, the REPL throws an Error with stack trace pointing to the offending line.
|
|
||||||
* Strings without a MIME default to text/plain.
|
|
||||||
* Objects are auto-JSON stringified and default to application/json unless a MIME is provided.
|
|
||||||
* Canvas images: Use toBlob() with await Promise wrapper
|
|
||||||
* Examples:
|
|
||||||
- await returnFile('data.txt', 'Hello World', 'text/plain')
|
|
||||||
- await returnFile('data.json', {key: 'value'}, 'application/json')
|
|
||||||
- await returnFile('data.csv', 'name,age\\nJohn,30', 'text/csv')
|
|
||||||
- Chart.js example:
|
|
||||||
const Chart = (await import('https://esm.run/chart.js/auto')).default;
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.width = 400; canvas.height = 300;
|
|
||||||
document.body.appendChild(canvas);
|
|
||||||
new Chart(canvas, {
|
|
||||||
type: 'line',
|
|
||||||
data: {
|
|
||||||
labels: ['Jan', 'Feb', 'Mar', 'Apr'],
|
|
||||||
datasets: [{ label: 'Sales', data: [10, 20, 15, 25], borderColor: 'blue' }]
|
|
||||||
},
|
},
|
||||||
options: { responsive: false, animation: false }
|
|
||||||
});
|
|
||||||
const blob = await new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
|
|
||||||
await returnFile('chart.png', blob, 'image/png');
|
|
||||||
|
|
||||||
Global variables:
|
|
||||||
- attachments[] - Array of attachment objects from user messages
|
|
||||||
* Properties:
|
|
||||||
- id: string (unique identifier)
|
|
||||||
- fileName: string (e.g., "data.xlsx")
|
|
||||||
- mimeType: string (e.g., "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
|
||||||
- size: number (bytes)
|
|
||||||
* Helper functions:
|
|
||||||
- listFiles() - Returns array of {id, fileName, mimeType, size} for all attachments
|
|
||||||
- readTextFile(attachmentId) - Returns text content of attachment (for CSV, JSON, text files)
|
|
||||||
- readBinaryFile(attachmentId) - Returns Uint8Array of binary data (for images, Excel, etc.)
|
|
||||||
* Examples:
|
|
||||||
- const files = listFiles();
|
|
||||||
- const csvContent = readTextFile(files[0].id); // Read CSV as text
|
|
||||||
- const xlsxBytes = readBinaryFile(files[0].id); // Read Excel as binary
|
|
||||||
- All standard browser globals (window, document, fetch, etc.)`,
|
|
||||||
parameters: javascriptReplSchema,
|
parameters: javascriptReplSchema,
|
||||||
execute: async function (_toolCallId: string, args: Static<typeof javascriptReplSchema>, signal?: AbortSignal) {
|
execute: async function (_toolCallId: string, args: Static<typeof javascriptReplSchema>, signal?: AbortSignal) {
|
||||||
const result = await executeJavaScript(
|
const result = await executeJavaScript(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue