@oh-my-pi/pi-web-ui 1.337.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +96 -0
- package/README.md +609 -0
- package/example/README.md +61 -0
- package/example/index.html +13 -0
- package/example/package.json +24 -0
- package/example/src/app.css +1 -0
- package/example/src/custom-messages.ts +99 -0
- package/example/src/main.ts +420 -0
- package/example/tsconfig.json +23 -0
- package/example/vite.config.ts +6 -0
- package/package.json +57 -0
- package/scripts/count-prompt-tokens.ts +88 -0
- package/src/ChatPanel.ts +218 -0
- package/src/app.css +68 -0
- package/src/components/AgentInterface.ts +390 -0
- package/src/components/AttachmentTile.ts +107 -0
- package/src/components/ConsoleBlock.ts +74 -0
- package/src/components/CustomProviderCard.ts +96 -0
- package/src/components/ExpandableSection.ts +46 -0
- package/src/components/Input.ts +113 -0
- package/src/components/MessageEditor.ts +404 -0
- package/src/components/MessageList.ts +97 -0
- package/src/components/Messages.ts +384 -0
- package/src/components/ProviderKeyInput.ts +152 -0
- package/src/components/SandboxedIframe.ts +626 -0
- package/src/components/StreamingMessageContainer.ts +107 -0
- package/src/components/ThinkingBlock.ts +45 -0
- package/src/components/message-renderer-registry.ts +28 -0
- package/src/components/sandbox/ArtifactsRuntimeProvider.ts +219 -0
- package/src/components/sandbox/AttachmentsRuntimeProvider.ts +66 -0
- package/src/components/sandbox/ConsoleRuntimeProvider.ts +186 -0
- package/src/components/sandbox/FileDownloadRuntimeProvider.ts +110 -0
- package/src/components/sandbox/RuntimeMessageBridge.ts +82 -0
- package/src/components/sandbox/RuntimeMessageRouter.ts +216 -0
- package/src/components/sandbox/SandboxRuntimeProvider.ts +52 -0
- package/src/dialogs/ApiKeyPromptDialog.ts +75 -0
- package/src/dialogs/AttachmentOverlay.ts +640 -0
- package/src/dialogs/CustomProviderDialog.ts +274 -0
- package/src/dialogs/ModelSelector.ts +314 -0
- package/src/dialogs/PersistentStorageDialog.ts +146 -0
- package/src/dialogs/ProvidersModelsTab.ts +212 -0
- package/src/dialogs/SessionListDialog.ts +157 -0
- package/src/dialogs/SettingsDialog.ts +216 -0
- package/src/index.ts +115 -0
- package/src/prompts/prompts.ts +282 -0
- package/src/storage/app-storage.ts +60 -0
- package/src/storage/backends/indexeddb-storage-backend.ts +193 -0
- package/src/storage/store.ts +33 -0
- package/src/storage/stores/custom-providers-store.ts +62 -0
- package/src/storage/stores/provider-keys-store.ts +33 -0
- package/src/storage/stores/sessions-store.ts +136 -0
- package/src/storage/stores/settings-store.ts +34 -0
- package/src/storage/types.ts +206 -0
- package/src/tools/artifacts/ArtifactElement.ts +14 -0
- package/src/tools/artifacts/ArtifactPill.ts +26 -0
- package/src/tools/artifacts/Console.ts +102 -0
- package/src/tools/artifacts/DocxArtifact.ts +213 -0
- package/src/tools/artifacts/ExcelArtifact.ts +231 -0
- package/src/tools/artifacts/GenericArtifact.ts +118 -0
- package/src/tools/artifacts/HtmlArtifact.ts +203 -0
- package/src/tools/artifacts/ImageArtifact.ts +116 -0
- package/src/tools/artifacts/MarkdownArtifact.ts +83 -0
- package/src/tools/artifacts/PdfArtifact.ts +201 -0
- package/src/tools/artifacts/SvgArtifact.ts +82 -0
- package/src/tools/artifacts/TextArtifact.ts +148 -0
- package/src/tools/artifacts/artifacts-tool-renderer.ts +371 -0
- package/src/tools/artifacts/artifacts.ts +713 -0
- package/src/tools/artifacts/index.ts +7 -0
- package/src/tools/extract-document.ts +271 -0
- package/src/tools/index.ts +46 -0
- package/src/tools/javascript-repl.ts +316 -0
- package/src/tools/renderer-registry.ts +127 -0
- package/src/tools/renderers/BashRenderer.ts +52 -0
- package/src/tools/renderers/CalculateRenderer.ts +58 -0
- package/src/tools/renderers/DefaultRenderer.ts +95 -0
- package/src/tools/renderers/GetCurrentTimeRenderer.ts +92 -0
- package/src/tools/types.ts +15 -0
- package/src/utils/attachment-utils.ts +472 -0
- package/src/utils/auth-token.ts +22 -0
- package/src/utils/format.ts +42 -0
- package/src/utils/i18n.ts +653 -0
- package/src/utils/model-discovery.ts +277 -0
- package/src/utils/proxy-utils.ts +134 -0
- package/src/utils/test-sessions.ts +2357 -0
- package/tsconfig.build.json +20 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { ArtifactElement } from "./ArtifactElement.js";
|
|
2
|
+
export { type Artifact, ArtifactsPanel, type ArtifactsParams } from "./artifacts.js";
|
|
3
|
+
export { ArtifactsToolRenderer } from "./artifacts-tool-renderer.js";
|
|
4
|
+
export { HtmlArtifact } from "./HtmlArtifact.js";
|
|
5
|
+
export { MarkdownArtifact } from "./MarkdownArtifact.js";
|
|
6
|
+
export { SvgArtifact } from "./SvgArtifact.js";
|
|
7
|
+
export { TextArtifact } from "./TextArtifact.js";
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import type { ToolResultMessage } from "@oh-my-pi/pi-ai";
|
|
3
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
4
|
+
import { html } from "lit";
|
|
5
|
+
import { createRef, ref } from "lit/directives/ref.js";
|
|
6
|
+
import { FileText } from "lucide";
|
|
7
|
+
import { EXTRACT_DOCUMENT_DESCRIPTION } from "../prompts/prompts.js";
|
|
8
|
+
import { loadAttachment } from "../utils/attachment-utils.js";
|
|
9
|
+
import { isCorsError } from "../utils/proxy-utils.js";
|
|
10
|
+
import { registerToolRenderer, renderCollapsibleHeader, renderHeader } from "./renderer-registry.js";
|
|
11
|
+
import type { ToolRenderer, ToolRenderResult } from "./types.js";
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// TYPES
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
const extractDocumentSchema = Type.Object({
|
|
18
|
+
url: Type.String({
|
|
19
|
+
description: "URL of the document to extract text from (PDF, DOCX, XLSX, or PPTX)",
|
|
20
|
+
}),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export type ExtractDocumentParams = Static<typeof extractDocumentSchema>;
|
|
24
|
+
|
|
25
|
+
export interface ExtractDocumentResult {
|
|
26
|
+
extractedText: string;
|
|
27
|
+
format: string;
|
|
28
|
+
fileName: string;
|
|
29
|
+
size: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// TOOL
|
|
34
|
+
// ============================================================================
|
|
35
|
+
|
|
36
|
+
export function createExtractDocumentTool(): AgentTool<typeof extractDocumentSchema, ExtractDocumentResult> & {
|
|
37
|
+
corsProxyUrl?: string;
|
|
38
|
+
} {
|
|
39
|
+
const tool = {
|
|
40
|
+
label: "Extract Document",
|
|
41
|
+
name: "extract_document",
|
|
42
|
+
corsProxyUrl: undefined as string | undefined, // Can be set by consumer (e.g., from user settings)
|
|
43
|
+
description: EXTRACT_DOCUMENT_DESCRIPTION,
|
|
44
|
+
parameters: extractDocumentSchema,
|
|
45
|
+
execute: async (_toolCallId: string, args: ExtractDocumentParams, signal?: AbortSignal) => {
|
|
46
|
+
if (signal?.aborted) {
|
|
47
|
+
throw new Error("Extract document aborted");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const url = args.url.trim();
|
|
51
|
+
if (!url) {
|
|
52
|
+
throw new Error("URL is required");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Validate URL format
|
|
56
|
+
try {
|
|
57
|
+
new URL(url);
|
|
58
|
+
} catch {
|
|
59
|
+
throw new Error(`Invalid URL: ${url}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Size limit: 50MB
|
|
63
|
+
const MAX_SIZE = 50 * 1024 * 1024;
|
|
64
|
+
|
|
65
|
+
// Helper function to fetch and process document
|
|
66
|
+
const fetchAndProcess = async (fetchUrl: string) => {
|
|
67
|
+
const response = await fetch(fetchUrl, { signal });
|
|
68
|
+
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`TELL USER: Unable to download the document (${response.status} ${response.statusText}). The site likely blocks automated downloads.\n\n` +
|
|
72
|
+
`INSTRUCT USER: Please download the file manually and attach it to your message using the attachment button (paperclip icon) in the message input area. I can then extract the text from the attached file.`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Check size before downloading
|
|
77
|
+
const contentLength = response.headers.get("content-length");
|
|
78
|
+
if (contentLength) {
|
|
79
|
+
const size = Number.parseInt(contentLength, 10);
|
|
80
|
+
if (size > MAX_SIZE) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`Document is too large (${(size / 1024 / 1024).toFixed(1)}MB). Maximum supported size is 50MB.`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Download the document
|
|
88
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
89
|
+
const size = arrayBuffer.byteLength;
|
|
90
|
+
|
|
91
|
+
if (size > MAX_SIZE) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`Document is too large (${(size / 1024 / 1024).toFixed(1)}MB). Maximum supported size is 50MB.`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return arrayBuffer;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Try without proxy first, fallback to proxy on CORS error
|
|
101
|
+
let arrayBuffer: ArrayBuffer;
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
// Attempt direct fetch first
|
|
105
|
+
arrayBuffer = await fetchAndProcess(url);
|
|
106
|
+
} catch (directError: any) {
|
|
107
|
+
// If CORS error and proxy is available, retry with proxy
|
|
108
|
+
if (isCorsError(directError) && tool.corsProxyUrl) {
|
|
109
|
+
try {
|
|
110
|
+
const proxiedUrl = tool.corsProxyUrl + encodeURIComponent(url);
|
|
111
|
+
arrayBuffer = await fetchAndProcess(proxiedUrl);
|
|
112
|
+
} catch (proxyError: any) {
|
|
113
|
+
// Proxy fetch also failed - throw helpful message
|
|
114
|
+
throw new Error(
|
|
115
|
+
`TELL USER: Unable to fetch the document due to CORS restrictions.\n\n` +
|
|
116
|
+
`Tried with proxy but it also failed: ${proxyError.message}\n\n` +
|
|
117
|
+
`INSTRUCT USER: Please download the file manually and attach it to your message using the attachment button (paperclip icon) in the message input area. I can then extract the text from the attached file.`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
} else if (isCorsError(directError) && !tool.corsProxyUrl) {
|
|
121
|
+
// CORS error but no proxy configured
|
|
122
|
+
throw new Error(
|
|
123
|
+
`TELL USER: Unable to fetch the document due to CORS restrictions (the server blocks requests from browser extensions).\n\n` +
|
|
124
|
+
`To fix this, you need to configure a CORS proxy in Sitegeist settings:\n` +
|
|
125
|
+
`1. Open Sitegeist settings\n` +
|
|
126
|
+
`2. Find "CORS Proxy URL" setting\n` +
|
|
127
|
+
`3. Enter a proxy URL like: https://corsproxy.io/?\n` +
|
|
128
|
+
`4. Save and try again\n\n` +
|
|
129
|
+
`Alternatively, download the file manually and attach it to your message using the attachment button (paperclip icon).`,
|
|
130
|
+
);
|
|
131
|
+
} else {
|
|
132
|
+
// Not a CORS error - re-throw
|
|
133
|
+
throw directError;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Extract filename from URL
|
|
138
|
+
const urlParts = url.split("/");
|
|
139
|
+
let fileName = urlParts[urlParts.length - 1]?.split("?")[0] || "document";
|
|
140
|
+
if (url.startsWith("https://arxiv.org/")) {
|
|
141
|
+
fileName = `${fileName}.pdf`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Use loadAttachment to process the document
|
|
145
|
+
const attachment = await loadAttachment(arrayBuffer, fileName);
|
|
146
|
+
|
|
147
|
+
if (!attachment.extractedText) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`Document format not supported. Supported formats:\n` +
|
|
150
|
+
`- PDF (.pdf)\n` +
|
|
151
|
+
`- Word (.docx)\n` +
|
|
152
|
+
`- Excel (.xlsx, .xls)\n` +
|
|
153
|
+
`- PowerPoint (.pptx)`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Determine format from attachment
|
|
158
|
+
let format = "unknown";
|
|
159
|
+
if (attachment.mimeType.includes("pdf")) {
|
|
160
|
+
format = "pdf";
|
|
161
|
+
} else if (attachment.mimeType.includes("wordprocessingml")) {
|
|
162
|
+
format = "docx";
|
|
163
|
+
} else if (attachment.mimeType.includes("spreadsheetml") || attachment.mimeType.includes("ms-excel")) {
|
|
164
|
+
format = "xlsx";
|
|
165
|
+
} else if (attachment.mimeType.includes("presentationml")) {
|
|
166
|
+
format = "pptx";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
content: [{ type: "text" as const, text: attachment.extractedText }],
|
|
171
|
+
details: {
|
|
172
|
+
extractedText: attachment.extractedText,
|
|
173
|
+
format,
|
|
174
|
+
fileName: attachment.fileName,
|
|
175
|
+
size: attachment.size,
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
return tool;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Export a default instance
|
|
184
|
+
export const extractDocumentTool = createExtractDocumentTool();
|
|
185
|
+
|
|
186
|
+
// ============================================================================
|
|
187
|
+
// RENDERER
|
|
188
|
+
// ============================================================================
|
|
189
|
+
|
|
190
|
+
export const extractDocumentRenderer: ToolRenderer<ExtractDocumentParams, ExtractDocumentResult> = {
|
|
191
|
+
render(
|
|
192
|
+
params: ExtractDocumentParams | undefined,
|
|
193
|
+
result: ToolResultMessage<ExtractDocumentResult> | undefined,
|
|
194
|
+
isStreaming?: boolean,
|
|
195
|
+
): ToolRenderResult {
|
|
196
|
+
// Determine status
|
|
197
|
+
const state = result ? (result.isError ? "error" : "complete") : isStreaming ? "inprogress" : "complete";
|
|
198
|
+
|
|
199
|
+
// Create refs for collapsible sections
|
|
200
|
+
const contentRef = createRef<HTMLDivElement>();
|
|
201
|
+
const chevronRef = createRef<HTMLSpanElement>();
|
|
202
|
+
|
|
203
|
+
// With result: show params + result
|
|
204
|
+
if (result && params) {
|
|
205
|
+
const details = result.details;
|
|
206
|
+
const title = details
|
|
207
|
+
? result.isError
|
|
208
|
+
? `Failed to extract ${details.fileName || "document"}`
|
|
209
|
+
: `Extracted text from ${details.fileName} (${details.format.toUpperCase()}, ${(
|
|
210
|
+
details.size / 1024
|
|
211
|
+
).toFixed(1)}KB)`
|
|
212
|
+
: result.isError
|
|
213
|
+
? "Failed to extract document"
|
|
214
|
+
: "Extracted text from document";
|
|
215
|
+
|
|
216
|
+
const output =
|
|
217
|
+
result.content
|
|
218
|
+
?.filter((c) => c.type === "text")
|
|
219
|
+
.map((c: any) => c.text)
|
|
220
|
+
.join("\n") || "";
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
content: html`
|
|
224
|
+
<div>
|
|
225
|
+
${renderCollapsibleHeader(state, FileText, title, contentRef, chevronRef, false)}
|
|
226
|
+
<div ${ref(contentRef)} class="max-h-0 overflow-hidden transition-all duration-300 space-y-3">
|
|
227
|
+
${
|
|
228
|
+
params.url
|
|
229
|
+
? html`<div class="text-sm text-gray-600 dark:text-gray-400"><strong>URL:</strong> ${params.url}</div>`
|
|
230
|
+
: ""
|
|
231
|
+
}
|
|
232
|
+
${output && !result.isError ? html`<code-block .code=${output} language="plaintext"></code-block>` : ""}
|
|
233
|
+
${
|
|
234
|
+
result.isError && output
|
|
235
|
+
? html`<console-block .content=${output} .variant=${"error"}></console-block>`
|
|
236
|
+
: ""
|
|
237
|
+
}
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
`,
|
|
241
|
+
isCustom: false,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Just params (streaming or waiting for result)
|
|
246
|
+
if (params) {
|
|
247
|
+
const title = "Extracting document...";
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
content: html`
|
|
251
|
+
<div>
|
|
252
|
+
${renderCollapsibleHeader(state, FileText, title, contentRef, chevronRef, false)}
|
|
253
|
+
<div ${ref(contentRef)} class="max-h-0 overflow-hidden transition-all duration-300">
|
|
254
|
+
<div class="text-sm text-gray-600 dark:text-gray-400"><strong>URL:</strong> ${params.url}</div>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
`,
|
|
258
|
+
isCustom: false,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// No params or result yet
|
|
263
|
+
return {
|
|
264
|
+
content: renderHeader(state, FileText, "Preparing extraction..."),
|
|
265
|
+
isCustom: false,
|
|
266
|
+
};
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// Auto-register the renderer
|
|
271
|
+
registerToolRenderer("extract_document", extractDocumentRenderer);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ToolResultMessage } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import "./javascript-repl.js"; // Auto-registers the renderer
|
|
3
|
+
import "./extract-document.js"; // Auto-registers the renderer
|
|
4
|
+
import { getToolRenderer, registerToolRenderer } from "./renderer-registry.js";
|
|
5
|
+
import { BashRenderer } from "./renderers/BashRenderer.js";
|
|
6
|
+
import { DefaultRenderer } from "./renderers/DefaultRenderer.js";
|
|
7
|
+
import type { ToolRenderResult } from "./types.js";
|
|
8
|
+
|
|
9
|
+
// Register all built-in tool renderers
|
|
10
|
+
registerToolRenderer("bash", new BashRenderer());
|
|
11
|
+
|
|
12
|
+
const defaultRenderer = new DefaultRenderer();
|
|
13
|
+
|
|
14
|
+
// Global flag to force default JSON rendering for all tools
|
|
15
|
+
let showJsonMode = false;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Enable or disable show JSON mode
|
|
19
|
+
* When enabled, all tool renderers will use the default JSON renderer
|
|
20
|
+
*/
|
|
21
|
+
export function setShowJsonMode(enabled: boolean): void {
|
|
22
|
+
showJsonMode = enabled;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Render tool - unified function that handles params, result, and streaming state
|
|
27
|
+
*/
|
|
28
|
+
export function renderTool(
|
|
29
|
+
toolName: string,
|
|
30
|
+
params: any | undefined,
|
|
31
|
+
result: ToolResultMessage | undefined,
|
|
32
|
+
isStreaming?: boolean,
|
|
33
|
+
): ToolRenderResult {
|
|
34
|
+
// If showJsonMode is enabled, always use the default renderer
|
|
35
|
+
if (showJsonMode) {
|
|
36
|
+
return defaultRenderer.render(params, result, isStreaming);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const renderer = getToolRenderer(toolName);
|
|
40
|
+
if (renderer) {
|
|
41
|
+
return renderer.render(params, result, isStreaming);
|
|
42
|
+
}
|
|
43
|
+
return defaultRenderer.render(params, result, isStreaming);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { getToolRenderer, registerToolRenderer };
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { i18n } from "@mariozechner/mini-lit";
|
|
2
|
+
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
3
|
+
import type { ToolResultMessage } from "@oh-my-pi/pi-ai";
|
|
4
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
5
|
+
import { html } from "lit";
|
|
6
|
+
import { createRef, ref } from "lit/directives/ref.js";
|
|
7
|
+
import { Code } from "lucide";
|
|
8
|
+
import { type SandboxFile, SandboxIframe, type SandboxResult } from "../components/SandboxedIframe.js";
|
|
9
|
+
import type { SandboxRuntimeProvider } from "../components/sandbox/SandboxRuntimeProvider.js";
|
|
10
|
+
import { JAVASCRIPT_REPL_TOOL_DESCRIPTION } from "../prompts/prompts.js";
|
|
11
|
+
import type { Attachment } from "../utils/attachment-utils.js";
|
|
12
|
+
import { registerToolRenderer, renderCollapsibleHeader, renderHeader } from "./renderer-registry.js";
|
|
13
|
+
import type { ToolRenderer, ToolRenderResult } from "./types.js";
|
|
14
|
+
|
|
15
|
+
// Execute JavaScript code with attachments using SandboxedIframe
|
|
16
|
+
export async function executeJavaScript(
|
|
17
|
+
code: string,
|
|
18
|
+
runtimeProviders: SandboxRuntimeProvider[],
|
|
19
|
+
signal?: AbortSignal,
|
|
20
|
+
sandboxUrlProvider?: () => string,
|
|
21
|
+
): Promise<{ output: string; files?: SandboxFile[] }> {
|
|
22
|
+
if (!code) {
|
|
23
|
+
throw new Error("Code parameter is required");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check for abort before starting
|
|
27
|
+
if (signal?.aborted) {
|
|
28
|
+
throw new Error("Execution aborted");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Create a SandboxedIframe instance for execution
|
|
32
|
+
const sandbox = new SandboxIframe();
|
|
33
|
+
if (sandboxUrlProvider) {
|
|
34
|
+
sandbox.sandboxUrlProvider = sandboxUrlProvider;
|
|
35
|
+
}
|
|
36
|
+
sandbox.style.display = "none";
|
|
37
|
+
document.body.appendChild(sandbox);
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const sandboxId = `repl-${Date.now()}-${Math.random().toString(36).substring(7)}`;
|
|
41
|
+
|
|
42
|
+
// Pass providers to execute (router handles all message routing)
|
|
43
|
+
// No additional consumers needed - execute() has its own internal consumer
|
|
44
|
+
const result: SandboxResult = await sandbox.execute(sandboxId, code, runtimeProviders, [], signal);
|
|
45
|
+
|
|
46
|
+
// Remove the sandbox iframe after execution
|
|
47
|
+
sandbox.remove();
|
|
48
|
+
|
|
49
|
+
// Build plain text response
|
|
50
|
+
let output = "";
|
|
51
|
+
|
|
52
|
+
// Add console output - result.console contains { type: string, text: string } from sandbox.js
|
|
53
|
+
if (result.console && result.console.length > 0) {
|
|
54
|
+
for (const entry of result.console) {
|
|
55
|
+
output += `${entry.text}\n`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Add error if execution failed
|
|
60
|
+
if (!result.success) {
|
|
61
|
+
if (output) output += "\n";
|
|
62
|
+
output += `Error: ${result.error?.message || "Unknown error"}\n${result.error?.stack || ""}`;
|
|
63
|
+
|
|
64
|
+
// Throw error so tool call is marked as failed
|
|
65
|
+
throw new Error(output.trim());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Add return value if present
|
|
69
|
+
if (result.returnValue !== undefined) {
|
|
70
|
+
if (output) output += "\n";
|
|
71
|
+
output += `=> ${
|
|
72
|
+
typeof result.returnValue === "object" ? JSON.stringify(result.returnValue, null, 2) : result.returnValue
|
|
73
|
+
}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Add file notifications
|
|
77
|
+
if (result.files && result.files.length > 0) {
|
|
78
|
+
output += `\n[Files returned: ${result.files.length}]\n`;
|
|
79
|
+
for (const file of result.files) {
|
|
80
|
+
output += ` - ${file.fileName} (${file.mimeType})\n`;
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
// Explicitly note when no files were returned (helpful for debugging)
|
|
84
|
+
if (code.includes("returnFile")) {
|
|
85
|
+
output += "\n[No files returned - check async operations]";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
output: output.trim() || "Code executed successfully (no output)",
|
|
91
|
+
files: result.files,
|
|
92
|
+
};
|
|
93
|
+
} catch (error: unknown) {
|
|
94
|
+
// Clean up on error
|
|
95
|
+
sandbox.remove();
|
|
96
|
+
throw new Error((error as Error).message || "Execution failed");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export type JavaScriptReplToolResult = {
|
|
101
|
+
files?:
|
|
102
|
+
| {
|
|
103
|
+
fileName: string;
|
|
104
|
+
contentBase64: string;
|
|
105
|
+
mimeType: string;
|
|
106
|
+
}[]
|
|
107
|
+
| undefined;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const javascriptReplSchema = Type.Object({
|
|
111
|
+
title: Type.String({
|
|
112
|
+
description:
|
|
113
|
+
"Brief title describing what the code snippet tries to achieve in active form, e.g. 'Calculating sum'",
|
|
114
|
+
}),
|
|
115
|
+
code: Type.String({ description: "JavaScript code to execute" }),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
export type JavaScriptReplParams = Static<typeof javascriptReplSchema>;
|
|
119
|
+
|
|
120
|
+
interface JavaScriptReplResult {
|
|
121
|
+
output?: string;
|
|
122
|
+
files?: Array<{
|
|
123
|
+
fileName: string;
|
|
124
|
+
mimeType: string;
|
|
125
|
+
size: number;
|
|
126
|
+
contentBase64: string;
|
|
127
|
+
}>;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function createJavaScriptReplTool(): AgentTool<typeof javascriptReplSchema, JavaScriptReplToolResult> & {
|
|
131
|
+
runtimeProvidersFactory?: () => SandboxRuntimeProvider[];
|
|
132
|
+
sandboxUrlProvider?: () => string;
|
|
133
|
+
} {
|
|
134
|
+
return {
|
|
135
|
+
label: "JavaScript REPL",
|
|
136
|
+
name: "javascript_repl",
|
|
137
|
+
runtimeProvidersFactory: () => [], // default to empty array
|
|
138
|
+
sandboxUrlProvider: undefined, // optional, for browser extensions
|
|
139
|
+
get description() {
|
|
140
|
+
const runtimeProviderDescriptions =
|
|
141
|
+
this.runtimeProvidersFactory?.()
|
|
142
|
+
.map((d) => d.getDescription())
|
|
143
|
+
.filter((d) => d.trim().length > 0) || [];
|
|
144
|
+
return JAVASCRIPT_REPL_TOOL_DESCRIPTION(runtimeProviderDescriptions);
|
|
145
|
+
},
|
|
146
|
+
parameters: javascriptReplSchema,
|
|
147
|
+
execute: async function (_toolCallId: string, args: Static<typeof javascriptReplSchema>, signal?: AbortSignal) {
|
|
148
|
+
const result = await executeJavaScript(
|
|
149
|
+
args.code,
|
|
150
|
+
this.runtimeProvidersFactory?.() ?? [],
|
|
151
|
+
signal,
|
|
152
|
+
this.sandboxUrlProvider,
|
|
153
|
+
);
|
|
154
|
+
// Convert files to JSON-serializable with base64 payloads
|
|
155
|
+
const files = (result.files || []).map((f) => {
|
|
156
|
+
const toBase64 = (input: string | Uint8Array): { base64: string; size: number } => {
|
|
157
|
+
if (input instanceof Uint8Array) {
|
|
158
|
+
let binary = "";
|
|
159
|
+
const chunk = 0x8000;
|
|
160
|
+
for (let i = 0; i < input.length; i += chunk) {
|
|
161
|
+
binary += String.fromCharCode(...input.subarray(i, i + chunk));
|
|
162
|
+
}
|
|
163
|
+
return { base64: btoa(binary), size: input.length };
|
|
164
|
+
} else if (typeof input === "string") {
|
|
165
|
+
const enc = new TextEncoder();
|
|
166
|
+
const bytes = enc.encode(input);
|
|
167
|
+
let binary = "";
|
|
168
|
+
const chunk = 0x8000;
|
|
169
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
170
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
171
|
+
}
|
|
172
|
+
return { base64: btoa(binary), size: bytes.length };
|
|
173
|
+
} else {
|
|
174
|
+
const s = String(input);
|
|
175
|
+
const enc = new TextEncoder();
|
|
176
|
+
const bytes = enc.encode(s);
|
|
177
|
+
let binary = "";
|
|
178
|
+
const chunk = 0x8000;
|
|
179
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
180
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
181
|
+
}
|
|
182
|
+
return { base64: btoa(binary), size: bytes.length };
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const { base64, size } = toBase64(f.content);
|
|
187
|
+
return {
|
|
188
|
+
fileName: f.fileName || "file",
|
|
189
|
+
mimeType: f.mimeType || "application/octet-stream",
|
|
190
|
+
size,
|
|
191
|
+
contentBase64: base64,
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
return { content: [{ type: "text", text: result.output }], details: { files } };
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Export a default instance for backward compatibility
|
|
200
|
+
export const javascriptReplTool = createJavaScriptReplTool();
|
|
201
|
+
|
|
202
|
+
export const javascriptReplRenderer: ToolRenderer<JavaScriptReplParams, JavaScriptReplResult> = {
|
|
203
|
+
render(
|
|
204
|
+
params: JavaScriptReplParams | undefined,
|
|
205
|
+
result: ToolResultMessage<JavaScriptReplResult> | undefined,
|
|
206
|
+
isStreaming?: boolean,
|
|
207
|
+
): ToolRenderResult {
|
|
208
|
+
// Determine status
|
|
209
|
+
const state = result ? (result.isError ? "error" : "complete") : isStreaming ? "inprogress" : "complete";
|
|
210
|
+
|
|
211
|
+
// Create refs for collapsible code section
|
|
212
|
+
const codeContentRef = createRef<HTMLDivElement>();
|
|
213
|
+
const codeChevronRef = createRef<HTMLSpanElement>();
|
|
214
|
+
|
|
215
|
+
// With result: show params + result
|
|
216
|
+
if (result && params) {
|
|
217
|
+
const output =
|
|
218
|
+
result.content
|
|
219
|
+
?.filter((c) => c.type === "text")
|
|
220
|
+
.map((c: any) => c.text)
|
|
221
|
+
.join("\n") || "";
|
|
222
|
+
const files = result.details?.files || [];
|
|
223
|
+
|
|
224
|
+
const attachments: Attachment[] = files.map((f, i) => {
|
|
225
|
+
// Decode base64 content for text files to show in overlay
|
|
226
|
+
let extractedText: string | undefined;
|
|
227
|
+
const isTextBased =
|
|
228
|
+
f.mimeType?.startsWith("text/") ||
|
|
229
|
+
f.mimeType === "application/json" ||
|
|
230
|
+
f.mimeType === "application/javascript" ||
|
|
231
|
+
f.mimeType?.includes("xml");
|
|
232
|
+
|
|
233
|
+
if (isTextBased && f.contentBase64) {
|
|
234
|
+
try {
|
|
235
|
+
extractedText = atob(f.contentBase64);
|
|
236
|
+
} catch (_e) {
|
|
237
|
+
console.warn("Failed to decode base64 content for", f.fileName);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
id: `repl-${Date.now()}-${i}`,
|
|
243
|
+
type: f.mimeType?.startsWith("image/") ? "image" : "document",
|
|
244
|
+
fileName: f.fileName || `file-${i}`,
|
|
245
|
+
mimeType: f.mimeType || "application/octet-stream",
|
|
246
|
+
size: f.size ?? 0,
|
|
247
|
+
content: f.contentBase64,
|
|
248
|
+
preview: f.mimeType?.startsWith("image/") ? f.contentBase64 : undefined,
|
|
249
|
+
extractedText,
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
content: html`
|
|
255
|
+
<div>
|
|
256
|
+
${renderCollapsibleHeader(
|
|
257
|
+
state,
|
|
258
|
+
Code,
|
|
259
|
+
params.title ? params.title : i18n("Executing JavaScript"),
|
|
260
|
+
codeContentRef,
|
|
261
|
+
codeChevronRef,
|
|
262
|
+
false,
|
|
263
|
+
)}
|
|
264
|
+
<div ${ref(codeContentRef)} class="max-h-0 overflow-hidden transition-all duration-300 space-y-3">
|
|
265
|
+
<code-block .code=${params.code || ""} language="javascript"></code-block>
|
|
266
|
+
${
|
|
267
|
+
output
|
|
268
|
+
? html`<console-block
|
|
269
|
+
.content=${output}
|
|
270
|
+
.variant=${result.isError ? "error" : "default"}
|
|
271
|
+
></console-block>`
|
|
272
|
+
: ""
|
|
273
|
+
}
|
|
274
|
+
</div>
|
|
275
|
+
${
|
|
276
|
+
attachments.length
|
|
277
|
+
? html`<div class="flex flex-wrap gap-2 mt-3">
|
|
278
|
+
${attachments.map((att) => html`<attachment-tile .attachment=${att}></attachment-tile>`)}
|
|
279
|
+
</div>`
|
|
280
|
+
: ""
|
|
281
|
+
}
|
|
282
|
+
</div>
|
|
283
|
+
`,
|
|
284
|
+
isCustom: false,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Just params (streaming or waiting for result)
|
|
289
|
+
if (params) {
|
|
290
|
+
return {
|
|
291
|
+
content: html`
|
|
292
|
+
<div>
|
|
293
|
+
${renderCollapsibleHeader(
|
|
294
|
+
state,
|
|
295
|
+
Code,
|
|
296
|
+
params.title ? params.title : i18n("Executing JavaScript"),
|
|
297
|
+
codeContentRef,
|
|
298
|
+
codeChevronRef,
|
|
299
|
+
false,
|
|
300
|
+
)}
|
|
301
|
+
<div ${ref(codeContentRef)} class="max-h-0 overflow-hidden transition-all duration-300">
|
|
302
|
+
${params.code ? html`<code-block .code=${params.code} language="javascript"></code-block>` : ""}
|
|
303
|
+
</div>
|
|
304
|
+
</div>
|
|
305
|
+
`,
|
|
306
|
+
isCustom: false,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// No params or result yet
|
|
311
|
+
return { content: renderHeader(state, Code, i18n("Preparing JavaScript...")), isCustom: false };
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
// Auto-register the renderer
|
|
316
|
+
registerToolRenderer(javascriptReplTool.name, javascriptReplRenderer);
|