@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.
Files changed (86) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +609 -0
  3. package/example/README.md +61 -0
  4. package/example/index.html +13 -0
  5. package/example/package.json +24 -0
  6. package/example/src/app.css +1 -0
  7. package/example/src/custom-messages.ts +99 -0
  8. package/example/src/main.ts +420 -0
  9. package/example/tsconfig.json +23 -0
  10. package/example/vite.config.ts +6 -0
  11. package/package.json +57 -0
  12. package/scripts/count-prompt-tokens.ts +88 -0
  13. package/src/ChatPanel.ts +218 -0
  14. package/src/app.css +68 -0
  15. package/src/components/AgentInterface.ts +390 -0
  16. package/src/components/AttachmentTile.ts +107 -0
  17. package/src/components/ConsoleBlock.ts +74 -0
  18. package/src/components/CustomProviderCard.ts +96 -0
  19. package/src/components/ExpandableSection.ts +46 -0
  20. package/src/components/Input.ts +113 -0
  21. package/src/components/MessageEditor.ts +404 -0
  22. package/src/components/MessageList.ts +97 -0
  23. package/src/components/Messages.ts +384 -0
  24. package/src/components/ProviderKeyInput.ts +152 -0
  25. package/src/components/SandboxedIframe.ts +626 -0
  26. package/src/components/StreamingMessageContainer.ts +107 -0
  27. package/src/components/ThinkingBlock.ts +45 -0
  28. package/src/components/message-renderer-registry.ts +28 -0
  29. package/src/components/sandbox/ArtifactsRuntimeProvider.ts +219 -0
  30. package/src/components/sandbox/AttachmentsRuntimeProvider.ts +66 -0
  31. package/src/components/sandbox/ConsoleRuntimeProvider.ts +186 -0
  32. package/src/components/sandbox/FileDownloadRuntimeProvider.ts +110 -0
  33. package/src/components/sandbox/RuntimeMessageBridge.ts +82 -0
  34. package/src/components/sandbox/RuntimeMessageRouter.ts +216 -0
  35. package/src/components/sandbox/SandboxRuntimeProvider.ts +52 -0
  36. package/src/dialogs/ApiKeyPromptDialog.ts +75 -0
  37. package/src/dialogs/AttachmentOverlay.ts +640 -0
  38. package/src/dialogs/CustomProviderDialog.ts +274 -0
  39. package/src/dialogs/ModelSelector.ts +314 -0
  40. package/src/dialogs/PersistentStorageDialog.ts +146 -0
  41. package/src/dialogs/ProvidersModelsTab.ts +212 -0
  42. package/src/dialogs/SessionListDialog.ts +157 -0
  43. package/src/dialogs/SettingsDialog.ts +216 -0
  44. package/src/index.ts +115 -0
  45. package/src/prompts/prompts.ts +282 -0
  46. package/src/storage/app-storage.ts +60 -0
  47. package/src/storage/backends/indexeddb-storage-backend.ts +193 -0
  48. package/src/storage/store.ts +33 -0
  49. package/src/storage/stores/custom-providers-store.ts +62 -0
  50. package/src/storage/stores/provider-keys-store.ts +33 -0
  51. package/src/storage/stores/sessions-store.ts +136 -0
  52. package/src/storage/stores/settings-store.ts +34 -0
  53. package/src/storage/types.ts +206 -0
  54. package/src/tools/artifacts/ArtifactElement.ts +14 -0
  55. package/src/tools/artifacts/ArtifactPill.ts +26 -0
  56. package/src/tools/artifacts/Console.ts +102 -0
  57. package/src/tools/artifacts/DocxArtifact.ts +213 -0
  58. package/src/tools/artifacts/ExcelArtifact.ts +231 -0
  59. package/src/tools/artifacts/GenericArtifact.ts +118 -0
  60. package/src/tools/artifacts/HtmlArtifact.ts +203 -0
  61. package/src/tools/artifacts/ImageArtifact.ts +116 -0
  62. package/src/tools/artifacts/MarkdownArtifact.ts +83 -0
  63. package/src/tools/artifacts/PdfArtifact.ts +201 -0
  64. package/src/tools/artifacts/SvgArtifact.ts +82 -0
  65. package/src/tools/artifacts/TextArtifact.ts +148 -0
  66. package/src/tools/artifacts/artifacts-tool-renderer.ts +371 -0
  67. package/src/tools/artifacts/artifacts.ts +713 -0
  68. package/src/tools/artifacts/index.ts +7 -0
  69. package/src/tools/extract-document.ts +271 -0
  70. package/src/tools/index.ts +46 -0
  71. package/src/tools/javascript-repl.ts +316 -0
  72. package/src/tools/renderer-registry.ts +127 -0
  73. package/src/tools/renderers/BashRenderer.ts +52 -0
  74. package/src/tools/renderers/CalculateRenderer.ts +58 -0
  75. package/src/tools/renderers/DefaultRenderer.ts +95 -0
  76. package/src/tools/renderers/GetCurrentTimeRenderer.ts +92 -0
  77. package/src/tools/types.ts +15 -0
  78. package/src/utils/attachment-utils.ts +472 -0
  79. package/src/utils/auth-token.ts +22 -0
  80. package/src/utils/format.ts +42 -0
  81. package/src/utils/i18n.ts +653 -0
  82. package/src/utils/model-discovery.ts +277 -0
  83. package/src/utils/proxy-utils.ts +134 -0
  84. package/src/utils/test-sessions.ts +2357 -0
  85. package/tsconfig.build.json +20 -0
  86. 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);