mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-16 12:03:23 +00:00
Replace window.sendRuntimeMessage existence check with URL-based detection. The sendRuntimeMessage function exists in both extension and non-extension contexts (e.g., downloaded HTML artifacts), causing incorrect behavior. Changes: - Add window.__isExtensionContext() helper to RuntimeMessageBridge that checks: - chrome-extension:// URLs (Chrome) - moz-extension:// URLs (Firefox) - about:srcdoc (sandbox iframes) - Update all runtime providers to use __isExtensionContext() instead: - FileDownloadRuntimeProvider: Correctly falls back to browser download - ConsoleRuntimeProvider: Only sends messages in extension context - ArtifactsRuntimeProvider: Properly detects offline/read-only mode This fixes the issue where downloaded HTML artifacts incorrectly try to communicate with the extension when opened from disk.
215 lines
6.1 KiB
TypeScript
215 lines
6.1 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).listArtifacts = async (): Promise<string[]> => {
|
|
// Online: ask extension
|
|
if ((window as any).__isExtensionContext?.()) {
|
|
const response = await (window as any).sendRuntimeMessage({
|
|
type: "artifact-operation",
|
|
action: "list",
|
|
});
|
|
if (!response.success) throw new Error(response.error);
|
|
return response.result;
|
|
}
|
|
// Offline: return snapshot keys
|
|
else {
|
|
return Object.keys((window as any).artifacts || {});
|
|
}
|
|
};
|
|
|
|
(window as any).getArtifact = async (filename: string): Promise<any> => {
|
|
let content: string;
|
|
|
|
// Online: ask extension
|
|
if ((window as any).__isExtensionContext?.()) {
|
|
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).__isExtensionContext?.()) {
|
|
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).__isExtensionContext?.()) {
|
|
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 "list": {
|
|
const filenames = Array.from(this.artifactsPanel.artifacts.keys());
|
|
respond({ success: true, result: filenames });
|
|
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;
|
|
}
|
|
}
|