@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,626 @@
1
+ import { LitElement } from "lit";
2
+ import { customElement, property } from "lit/decorators.js";
3
+ import { ConsoleRuntimeProvider } from "./sandbox/ConsoleRuntimeProvider.js";
4
+ import { RuntimeMessageBridge } from "./sandbox/RuntimeMessageBridge.js";
5
+ import { type MessageConsumer, RUNTIME_MESSAGE_ROUTER } from "./sandbox/RuntimeMessageRouter.js";
6
+ import type { SandboxRuntimeProvider } from "./sandbox/SandboxRuntimeProvider.js";
7
+
8
+ export interface SandboxFile {
9
+ fileName: string;
10
+ content: string | Uint8Array;
11
+ mimeType: string;
12
+ }
13
+
14
+ export interface SandboxResult {
15
+ success: boolean;
16
+ console: Array<{ type: string; text: string }>;
17
+ files?: SandboxFile[];
18
+ error?: { message: string; stack: string };
19
+ returnValue?: any;
20
+ }
21
+
22
+ /**
23
+ * Function that returns the URL to the sandbox HTML file.
24
+ * Used in browser extensions to load sandbox.html via chrome.runtime.getURL().
25
+ */
26
+ export type SandboxUrlProvider = () => string;
27
+
28
+ /**
29
+ * Configuration for prepareHtmlDocument
30
+ */
31
+ export interface PrepareHtmlOptions {
32
+ /** True if this is an HTML artifact (inject into existing HTML), false if REPL (wrap in HTML) */
33
+ isHtmlArtifact: boolean;
34
+ /** True if this is a standalone download (no runtime bridge, no navigation interceptor) */
35
+ isStandalone?: boolean;
36
+ }
37
+
38
+ /**
39
+ * Escape HTML special sequences in code to prevent premature tag closure
40
+ * @param code Code that will be injected into <script> tags
41
+ * @returns Escaped code safe for injection
42
+ */
43
+ function escapeScriptContent(code: string): string {
44
+ return code.replace(/<\/script/gi, "<\\/script");
45
+ }
46
+
47
+ @customElement("sandbox-iframe")
48
+ export class SandboxIframe extends LitElement {
49
+ private iframe?: HTMLIFrameElement;
50
+
51
+ /**
52
+ * Optional: Provide a function that returns the sandbox HTML URL.
53
+ * If provided, the iframe will use this URL instead of srcdoc.
54
+ * This is required for browser extensions with strict CSP.
55
+ */
56
+ @property({ attribute: false }) sandboxUrlProvider?: SandboxUrlProvider;
57
+
58
+ createRenderRoot() {
59
+ return this;
60
+ }
61
+
62
+ override connectedCallback() {
63
+ super.connectedCallback();
64
+ }
65
+
66
+ override disconnectedCallback() {
67
+ super.disconnectedCallback();
68
+ // Note: We don't unregister the sandbox here for loadContent() mode
69
+ // because the caller (HtmlArtifact) owns the sandbox lifecycle.
70
+ // For execute() mode, the sandbox is unregistered in the cleanup function.
71
+ this.iframe?.remove();
72
+ }
73
+
74
+ /**
75
+ * Load HTML content into sandbox and keep it displayed (for HTML artifacts)
76
+ * @param sandboxId Unique ID
77
+ * @param htmlContent Full HTML content
78
+ * @param providers Runtime providers to inject
79
+ * @param consumers Message consumers to register (optional)
80
+ */
81
+ public loadContent(
82
+ sandboxId: string,
83
+ htmlContent: string,
84
+ providers: SandboxRuntimeProvider[] = [],
85
+ consumers: MessageConsumer[] = [],
86
+ ): void {
87
+ // Unregister previous sandbox if exists
88
+ try {
89
+ RUNTIME_MESSAGE_ROUTER.unregisterSandbox(sandboxId);
90
+ } catch {
91
+ // Sandbox might not exist, that's ok
92
+ }
93
+
94
+ providers = [new ConsoleRuntimeProvider(), ...providers];
95
+
96
+ RUNTIME_MESSAGE_ROUTER.registerSandbox(sandboxId, providers, consumers);
97
+
98
+ // loadContent is always used for HTML artifacts (not standalone)
99
+ const completeHtml = this.prepareHtmlDocument(sandboxId, htmlContent, providers, {
100
+ isHtmlArtifact: true,
101
+ isStandalone: false,
102
+ });
103
+
104
+ // Validate HTML before loading
105
+ const validationError = this.validateHtml(completeHtml);
106
+ if (validationError) {
107
+ console.error("HTML validation failed:", validationError);
108
+ // Show error in iframe instead of crashing
109
+ this.iframe?.remove();
110
+ this.iframe = document.createElement("iframe");
111
+ this.iframe.style.cssText = "width: 100%; height: 100%; border: none;";
112
+ this.iframe.srcdoc = `
113
+ <html>
114
+ <body style="font-family: monospace; padding: 20px; background: #fff; color: #000;">
115
+ <h3 style="color: #c00;">HTML Validation Error</h3>
116
+ <pre style="background: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto; white-space: pre-wrap;">${validationError}</pre>
117
+ </body>
118
+ </html>
119
+ `;
120
+ this.appendChild(this.iframe);
121
+ return;
122
+ }
123
+
124
+ // Remove previous iframe if exists
125
+ this.iframe?.remove();
126
+
127
+ if (this.sandboxUrlProvider) {
128
+ // Browser extension mode: use sandbox.html with postMessage
129
+ this.loadViaSandboxUrl(sandboxId, completeHtml);
130
+ } else {
131
+ // Web mode: use srcdoc
132
+ this.loadViaSrcdoc(sandboxId, completeHtml);
133
+ }
134
+ }
135
+
136
+ private loadViaSandboxUrl(sandboxId: string, completeHtml: string): void {
137
+ // Create iframe pointing to sandbox URL
138
+ this.iframe = document.createElement("iframe");
139
+ this.iframe.sandbox.add("allow-scripts");
140
+ this.iframe.sandbox.add("allow-modals");
141
+ this.iframe.style.width = "100%";
142
+ this.iframe.style.height = "100%";
143
+ this.iframe.style.border = "none";
144
+ this.iframe.src = this.sandboxUrlProvider!();
145
+
146
+ // Update router with iframe reference BEFORE appending to DOM
147
+ RUNTIME_MESSAGE_ROUTER.setSandboxIframe(sandboxId, this.iframe);
148
+
149
+ // Listen for open-external-url messages from iframe
150
+ const externalUrlHandler = (e: MessageEvent) => {
151
+ if (e.data.type === "open-external-url" && e.source === this.iframe?.contentWindow) {
152
+ // Use chrome.tabs API to open in new tab
153
+ const chromeAPI = (globalThis as any).chrome;
154
+ if (chromeAPI?.tabs) {
155
+ chromeAPI.tabs.create({ url: e.data.url });
156
+ } else {
157
+ // Fallback for non-extension context
158
+ window.open(e.data.url, "_blank");
159
+ }
160
+ }
161
+ };
162
+ window.addEventListener("message", externalUrlHandler);
163
+
164
+ // Listen for sandbox-ready and sandbox-error messages directly
165
+ const readyHandler = (e: MessageEvent) => {
166
+ if (e.data.type === "sandbox-ready" && e.source === this.iframe?.contentWindow) {
167
+ window.removeEventListener("message", readyHandler);
168
+ window.removeEventListener("message", errorHandler);
169
+
170
+ // Send content to sandbox
171
+ this.iframe?.contentWindow?.postMessage(
172
+ {
173
+ type: "sandbox-load",
174
+ sandboxId,
175
+ code: completeHtml,
176
+ },
177
+ "*",
178
+ );
179
+ }
180
+ };
181
+
182
+ const errorHandler = (e: MessageEvent) => {
183
+ if (e.data.type === "sandbox-error" && e.source === this.iframe?.contentWindow) {
184
+ window.removeEventListener("message", readyHandler);
185
+ window.removeEventListener("message", errorHandler);
186
+
187
+ // The sandbox.js already sent us the error via postMessage.
188
+ // We need to convert it to an execution-error message that the execute() consumer will handle.
189
+ // Simulate receiving an execution-error from the sandbox
190
+ window.postMessage(
191
+ {
192
+ sandboxId: sandboxId,
193
+ type: "execution-error",
194
+ error: { message: e.data.error, stack: e.data.stack },
195
+ },
196
+ "*",
197
+ );
198
+ }
199
+ };
200
+
201
+ window.addEventListener("message", readyHandler);
202
+ window.addEventListener("message", errorHandler);
203
+
204
+ this.appendChild(this.iframe);
205
+ }
206
+
207
+ private loadViaSrcdoc(sandboxId: string, completeHtml: string): void {
208
+ // Create iframe with srcdoc
209
+ this.iframe = document.createElement("iframe");
210
+ this.iframe.sandbox.add("allow-scripts");
211
+ this.iframe.sandbox.add("allow-modals");
212
+ this.iframe.style.width = "100%";
213
+ this.iframe.style.height = "100%";
214
+ this.iframe.style.border = "none";
215
+ this.iframe.srcdoc = completeHtml;
216
+
217
+ // Update router with iframe reference BEFORE appending to DOM
218
+ RUNTIME_MESSAGE_ROUTER.setSandboxIframe(sandboxId, this.iframe);
219
+
220
+ // Listen for open-external-url messages from iframe
221
+ const externalUrlHandler = (e: MessageEvent) => {
222
+ if (e.data.type === "open-external-url" && e.source === this.iframe?.contentWindow) {
223
+ // Fallback for non-extension context
224
+ window.open(e.data.url, "_blank");
225
+ }
226
+ };
227
+ window.addEventListener("message", externalUrlHandler);
228
+
229
+ this.appendChild(this.iframe);
230
+ }
231
+
232
+ /**
233
+ * Execute code in sandbox
234
+ * @param sandboxId Unique ID for this execution
235
+ * @param code User code (plain JS for REPL, or full HTML for artifacts)
236
+ * @param providers Runtime providers to inject
237
+ * @param consumers Additional message consumers (optional, execute has its own internal consumer)
238
+ * @param signal Abort signal
239
+ * @returns Promise resolving to execution result
240
+ */
241
+ public async execute(
242
+ sandboxId: string,
243
+ code: string,
244
+ providers: SandboxRuntimeProvider[] = [],
245
+ consumers: MessageConsumer[] = [],
246
+ signal?: AbortSignal,
247
+ isHtmlArtifact: boolean = false,
248
+ ): Promise<SandboxResult> {
249
+ if (signal?.aborted) {
250
+ throw new Error("Execution aborted");
251
+ }
252
+
253
+ const consoleProvider = new ConsoleRuntimeProvider();
254
+ providers = [consoleProvider, ...providers];
255
+ RUNTIME_MESSAGE_ROUTER.registerSandbox(sandboxId, providers, consumers);
256
+
257
+ // Notify providers that execution is starting
258
+ for (const provider of providers) {
259
+ provider.onExecutionStart?.(sandboxId, signal);
260
+ }
261
+
262
+ const files: SandboxFile[] = [];
263
+ let completed = false;
264
+
265
+ return new Promise((resolve, reject) => {
266
+ // 4. Create execution consumer for lifecycle messages
267
+ const executionConsumer: MessageConsumer = {
268
+ async handleMessage(message: any): Promise<void> {
269
+ if (message.type === "file-returned") {
270
+ files.push({
271
+ fileName: message.fileName,
272
+ content: message.content,
273
+ mimeType: message.mimeType,
274
+ });
275
+ } else if (message.type === "execution-complete") {
276
+ completed = true;
277
+ cleanup();
278
+ resolve({
279
+ success: true,
280
+ console: consoleProvider.getLogs(),
281
+ files,
282
+ returnValue: message.returnValue,
283
+ });
284
+ } else if (message.type === "execution-error") {
285
+ completed = true;
286
+ cleanup();
287
+ resolve({ success: false, console: consoleProvider.getLogs(), error: message.error, files });
288
+ }
289
+ },
290
+ };
291
+
292
+ RUNTIME_MESSAGE_ROUTER.addConsumer(sandboxId, executionConsumer);
293
+
294
+ const cleanup = () => {
295
+ // Notify providers that execution has ended
296
+ for (const provider of providers) {
297
+ provider.onExecutionEnd?.(sandboxId);
298
+ }
299
+
300
+ RUNTIME_MESSAGE_ROUTER.unregisterSandbox(sandboxId);
301
+ signal?.removeEventListener("abort", abortHandler);
302
+ clearTimeout(timeoutId);
303
+ this.iframe?.remove();
304
+ this.iframe = undefined;
305
+ };
306
+
307
+ // Abort handler
308
+ const abortHandler = () => {
309
+ if (!completed) {
310
+ completed = true;
311
+ cleanup();
312
+ reject(new Error("Execution aborted"));
313
+ }
314
+ };
315
+
316
+ if (signal) {
317
+ signal.addEventListener("abort", abortHandler);
318
+ }
319
+
320
+ // Timeout handler (30 seconds)
321
+ const timeoutId = setTimeout(() => {
322
+ if (!completed) {
323
+ completed = true;
324
+ cleanup();
325
+ resolve({
326
+ success: false,
327
+ console: consoleProvider.getLogs(),
328
+ error: { message: "Execution timeout (120s)", stack: "" },
329
+ files,
330
+ });
331
+ }
332
+ }, 120000);
333
+
334
+ // 4. Prepare HTML and create iframe
335
+ const completeHtml = this.prepareHtmlDocument(sandboxId, code, providers, {
336
+ isHtmlArtifact,
337
+ isStandalone: false,
338
+ });
339
+
340
+ // 5. Validate HTML before sending to sandbox
341
+ const validationError = this.validateHtml(completeHtml);
342
+ if (validationError) {
343
+ reject(new Error(`HTML validation failed: ${validationError}`));
344
+ return;
345
+ }
346
+
347
+ if (this.sandboxUrlProvider) {
348
+ // Browser extension mode: wait for sandbox-ready
349
+ this.iframe = document.createElement("iframe");
350
+ this.iframe.sandbox.add("allow-scripts", "allow-modals");
351
+ this.iframe.style.cssText = "width: 100%; height: 100%; border: none;";
352
+ this.iframe.src = this.sandboxUrlProvider();
353
+
354
+ // Update router with iframe reference BEFORE appending to DOM
355
+ RUNTIME_MESSAGE_ROUTER.setSandboxIframe(sandboxId, this.iframe);
356
+
357
+ // Listen for sandbox-ready and sandbox-error messages
358
+ const readyHandler = (e: MessageEvent) => {
359
+ if (e.data.type === "sandbox-ready" && e.source === this.iframe?.contentWindow) {
360
+ window.removeEventListener("message", readyHandler);
361
+ window.removeEventListener("message", errorHandler);
362
+
363
+ // Send content to sandbox
364
+ this.iframe?.contentWindow?.postMessage(
365
+ {
366
+ type: "sandbox-load",
367
+ sandboxId,
368
+ code: completeHtml,
369
+ },
370
+ "*",
371
+ );
372
+ }
373
+ };
374
+
375
+ const errorHandler = (e: MessageEvent) => {
376
+ if (e.data.type === "sandbox-error" && e.source === this.iframe?.contentWindow) {
377
+ window.removeEventListener("message", readyHandler);
378
+ window.removeEventListener("message", errorHandler);
379
+
380
+ // Convert sandbox-error to execution-error for the execution consumer
381
+ window.postMessage(
382
+ {
383
+ sandboxId: sandboxId,
384
+ type: "execution-error",
385
+ error: { message: e.data.error, stack: e.data.stack },
386
+ },
387
+ "*",
388
+ );
389
+ }
390
+ };
391
+
392
+ window.addEventListener("message", readyHandler);
393
+ window.addEventListener("message", errorHandler);
394
+
395
+ this.appendChild(this.iframe);
396
+ } else {
397
+ // Web mode: use srcdoc
398
+ this.iframe = document.createElement("iframe");
399
+ this.iframe.sandbox.add("allow-scripts", "allow-modals");
400
+ this.iframe.style.cssText = "width: 100%; height: 100%; border: none; display: none;";
401
+ this.iframe.srcdoc = completeHtml;
402
+
403
+ // Update router with iframe reference BEFORE appending to DOM
404
+ RUNTIME_MESSAGE_ROUTER.setSandboxIframe(sandboxId, this.iframe);
405
+
406
+ this.appendChild(this.iframe);
407
+ }
408
+ });
409
+ }
410
+
411
+ /**
412
+ * Validate HTML using DOMParser - returns error message if invalid, null if valid
413
+ * Note: JavaScript syntax validation is done in sandbox.js to avoid CSP restrictions
414
+ */
415
+ private validateHtml(html: string): string | null {
416
+ try {
417
+ const parser = new DOMParser();
418
+ const doc = parser.parseFromString(html, "text/html");
419
+
420
+ // Check for parser errors
421
+ const parserError = doc.querySelector("parsererror");
422
+ if (parserError) {
423
+ return parserError.textContent || "Unknown parse error";
424
+ }
425
+
426
+ return null;
427
+ } catch (error: any) {
428
+ return error.message || "Unknown validation error";
429
+ }
430
+ }
431
+
432
+ /**
433
+ * Prepare complete HTML document with runtime + user code
434
+ * PUBLIC so HtmlArtifact can use it for download button
435
+ */
436
+ public prepareHtmlDocument(
437
+ sandboxId: string,
438
+ userCode: string,
439
+ providers: SandboxRuntimeProvider[] = [],
440
+ options?: PrepareHtmlOptions,
441
+ ): string {
442
+ // Default options
443
+ const opts: PrepareHtmlOptions = {
444
+ isHtmlArtifact: false,
445
+ isStandalone: false,
446
+ ...options,
447
+ };
448
+
449
+ // Runtime script that will be injected
450
+ const runtime = this.getRuntimeScript(sandboxId, providers, opts.isStandalone || false);
451
+
452
+ // Only check for HTML tags if explicitly marked as HTML artifact
453
+ // For javascript_repl, userCode is JavaScript that may contain HTML in string literals
454
+ if (opts.isHtmlArtifact) {
455
+ // HTML Artifact - inject runtime into existing HTML
456
+ const headMatch = userCode.match(/<head[^>]*>/i);
457
+ if (headMatch) {
458
+ const index = headMatch.index! + headMatch[0].length;
459
+ return userCode.slice(0, index) + runtime + userCode.slice(index);
460
+ }
461
+
462
+ const htmlMatch = userCode.match(/<html[^>]*>/i);
463
+ if (htmlMatch) {
464
+ const index = htmlMatch.index! + htmlMatch[0].length;
465
+ return userCode.slice(0, index) + runtime + userCode.slice(index);
466
+ }
467
+
468
+ // Fallback: prepend runtime
469
+ return runtime + userCode;
470
+ } else {
471
+ // REPL - wrap code in HTML with runtime and call complete() when done
472
+ // Escape </script> in user code to prevent premature tag closure
473
+ const escapedUserCode = escapeScriptContent(userCode);
474
+
475
+ return `<!DOCTYPE html>
476
+ <html>
477
+ <head>
478
+ ${runtime}
479
+ </head>
480
+ <body>
481
+ <script type="module">
482
+ (async () => {
483
+ try {
484
+ // Wrap user code in async function to capture return value
485
+ const userCodeFunc = async () => {
486
+ ${escapedUserCode}
487
+ };
488
+
489
+ const returnValue = await userCodeFunc();
490
+
491
+ // Call completion callbacks before complete()
492
+ if (window.__completionCallbacks && window.__completionCallbacks.length > 0) {
493
+ try {
494
+ await Promise.all(window.__completionCallbacks.map(cb => cb(true)));
495
+ } catch (e) {
496
+ console.error('Completion callback error:', e);
497
+ }
498
+ }
499
+
500
+ await window.complete(null, returnValue);
501
+ } catch (error) {
502
+
503
+ // Call completion callbacks before complete() (error path)
504
+ if (window.__completionCallbacks && window.__completionCallbacks.length > 0) {
505
+ try {
506
+ await Promise.all(window.__completionCallbacks.map(cb => cb(false)));
507
+ } catch (e) {
508
+ console.error('Completion callback error:', e);
509
+ }
510
+ }
511
+
512
+ await window.complete({
513
+ message: error?.message || String(error),
514
+ stack: error?.stack || new Error().stack
515
+ });
516
+ }
517
+ })();
518
+ </script>
519
+ </body>
520
+ </html>`;
521
+ }
522
+ }
523
+
524
+ /**
525
+ * Generate runtime script from providers
526
+ * @param sandboxId Unique sandbox ID
527
+ * @param providers Runtime providers
528
+ * @param isStandalone If true, skip runtime bridge and navigation interceptor (for standalone downloads)
529
+ */
530
+ private getRuntimeScript(
531
+ sandboxId: string,
532
+ providers: SandboxRuntimeProvider[] = [],
533
+ isStandalone: boolean = false,
534
+ ): string {
535
+ // Collect all data from providers
536
+ const allData: Record<string, any> = {};
537
+ for (const provider of providers) {
538
+ Object.assign(allData, provider.getData());
539
+ }
540
+
541
+ // Generate bridge code (skip if standalone)
542
+ const bridgeCode = isStandalone
543
+ ? ""
544
+ : RuntimeMessageBridge.generateBridgeCode({
545
+ context: "sandbox-iframe",
546
+ sandboxId,
547
+ });
548
+
549
+ // Collect all runtime functions - pass sandboxId as string literal
550
+ const runtimeFunctions: string[] = [];
551
+ for (const provider of providers) {
552
+ runtimeFunctions.push(`(${provider.getRuntime().toString()})(${JSON.stringify(sandboxId)});`);
553
+ }
554
+
555
+ // Build script with HTML escaping
556
+ // Escape </script> to prevent premature tag closure in HTML parser
557
+ const dataInjection = Object.entries(allData)
558
+ .map(([key, value]) => {
559
+ const jsonStr = JSON.stringify(value).replace(/<\/script/gi, "<\\/script");
560
+ return `window.${key} = ${jsonStr};`;
561
+ })
562
+ .join("\n");
563
+
564
+ // TODO the font-size is needed, as chrome seems to inject a stylesheet into iframes
565
+ // found in an extension context like sidepanel, settin body { font-size: 75% }. It's
566
+ // definitely not our code doing that.
567
+ // See https://stackoverflow.com/questions/71480433/chrome-is-injecting-some-stylesheet-in-popup-ui-which-reduces-the-font-size-to-7
568
+
569
+ // Navigation interceptor (only if NOT standalone)
570
+ const navigationInterceptor = isStandalone
571
+ ? ""
572
+ : `
573
+ // Navigation interceptor: prevent all navigation and open externally
574
+ (function() {
575
+ // Intercept link clicks
576
+ document.addEventListener('click', function(e) {
577
+ const link = e.target.closest('a');
578
+ if (link && link.href) {
579
+ // Check if it's an external link (not javascript: or #hash)
580
+ if (link.href.startsWith('http://') || link.href.startsWith('https://')) {
581
+ e.preventDefault();
582
+ e.stopPropagation();
583
+ window.parent.postMessage({ type: 'open-external-url', url: link.href }, '*');
584
+ }
585
+ }
586
+ }, true);
587
+
588
+ // Intercept form submissions
589
+ document.addEventListener('submit', function(e) {
590
+ const form = e.target;
591
+ if (form && form.action) {
592
+ e.preventDefault();
593
+ e.stopPropagation();
594
+ window.parent.postMessage({ type: 'open-external-url', url: form.action }, '*');
595
+ }
596
+ }, true);
597
+
598
+ // Prevent window.location changes (only if not already redefined)
599
+ try {
600
+ const originalLocation = window.location;
601
+ Object.defineProperty(window, 'location', {
602
+ get: function() { return originalLocation; },
603
+ set: function(url) {
604
+ window.parent.postMessage({ type: 'open-external-url', url: url.toString() }, '*');
605
+ }
606
+ });
607
+ } catch (e) {
608
+ // Already defined, skip
609
+ }
610
+ })();
611
+ `;
612
+
613
+ return `<style>
614
+ html, body {
615
+ font-size: initial;
616
+ }
617
+ </style>
618
+ <script>
619
+ window.sandboxId = ${JSON.stringify(sandboxId)};
620
+ ${dataInjection}
621
+ ${bridgeCode}
622
+ ${runtimeFunctions.join("\n")}
623
+ ${navigationInterceptor}
624
+ </script>`;
625
+ }
626
+ }