@mariozechner/pi-web-ui 0.5.44

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 (265) hide show
  1. package/README.md +252 -0
  2. package/dist/ChatPanel.d.ts +23 -0
  3. package/dist/ChatPanel.d.ts.map +1 -0
  4. package/dist/ChatPanel.js +224 -0
  5. package/dist/ChatPanel.js.map +1 -0
  6. package/dist/app.css +2 -0
  7. package/dist/components/AgentInterface.d.ts +35 -0
  8. package/dist/components/AgentInterface.d.ts.map +1 -0
  9. package/dist/components/AgentInterface.js +308 -0
  10. package/dist/components/AgentInterface.js.map +1 -0
  11. package/dist/components/AttachmentTile.d.ts +12 -0
  12. package/dist/components/AttachmentTile.d.ts.map +1 -0
  13. package/dist/components/AttachmentTile.js +114 -0
  14. package/dist/components/AttachmentTile.js.map +1 -0
  15. package/dist/components/ConsoleBlock.d.ts +11 -0
  16. package/dist/components/ConsoleBlock.d.ts.map +1 -0
  17. package/dist/components/ConsoleBlock.js +77 -0
  18. package/dist/components/ConsoleBlock.js.map +1 -0
  19. package/dist/components/Input.d.ts +26 -0
  20. package/dist/components/Input.d.ts.map +1 -0
  21. package/dist/components/Input.js +56 -0
  22. package/dist/components/Input.js.map +1 -0
  23. package/dist/components/MessageEditor.d.ts +38 -0
  24. package/dist/components/MessageEditor.d.ts.map +1 -0
  25. package/dist/components/MessageEditor.js +296 -0
  26. package/dist/components/MessageEditor.js.map +1 -0
  27. package/dist/components/MessageList.d.ts +13 -0
  28. package/dist/components/MessageList.d.ts.map +1 -0
  29. package/dist/components/MessageList.js +88 -0
  30. package/dist/components/MessageList.js.map +1 -0
  31. package/dist/components/Messages.d.ts +53 -0
  32. package/dist/components/Messages.d.ts.map +1 -0
  33. package/dist/components/Messages.js +323 -0
  34. package/dist/components/Messages.js.map +1 -0
  35. package/dist/components/ProviderKeyInput.d.ts +16 -0
  36. package/dist/components/ProviderKeyInput.d.ts.map +1 -0
  37. package/dist/components/ProviderKeyInput.js +183 -0
  38. package/dist/components/ProviderKeyInput.js.map +1 -0
  39. package/dist/components/SandboxedIframe.d.ts +63 -0
  40. package/dist/components/SandboxedIframe.d.ts.map +1 -0
  41. package/dist/components/SandboxedIframe.js +435 -0
  42. package/dist/components/SandboxedIframe.js.map +1 -0
  43. package/dist/components/StreamingMessageContainer.d.ts +17 -0
  44. package/dist/components/StreamingMessageContainer.d.ts.map +1 -0
  45. package/dist/components/StreamingMessageContainer.js +114 -0
  46. package/dist/components/StreamingMessageContainer.js.map +1 -0
  47. package/dist/dialogs/ApiKeyPromptDialog.d.ts +15 -0
  48. package/dist/dialogs/ApiKeyPromptDialog.d.ts.map +1 -0
  49. package/dist/dialogs/ApiKeyPromptDialog.js +80 -0
  50. package/dist/dialogs/ApiKeyPromptDialog.js.map +1 -0
  51. package/dist/dialogs/AttachmentOverlay.d.ts +32 -0
  52. package/dist/dialogs/AttachmentOverlay.d.ts.map +1 -0
  53. package/dist/dialogs/AttachmentOverlay.js +575 -0
  54. package/dist/dialogs/AttachmentOverlay.js.map +1 -0
  55. package/dist/dialogs/ModelSelector.d.ts +27 -0
  56. package/dist/dialogs/ModelSelector.d.ts.map +1 -0
  57. package/dist/dialogs/ModelSelector.js +334 -0
  58. package/dist/dialogs/ModelSelector.js.map +1 -0
  59. package/dist/dialogs/SettingsDialog.d.ts +31 -0
  60. package/dist/dialogs/SettingsDialog.d.ts.map +1 -0
  61. package/dist/dialogs/SettingsDialog.js +228 -0
  62. package/dist/dialogs/SettingsDialog.js.map +1 -0
  63. package/dist/index.d.ts +46 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +51 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/state/agent-session.d.ts +58 -0
  68. package/dist/state/agent-session.d.ts.map +1 -0
  69. package/dist/state/agent-session.js +252 -0
  70. package/dist/state/agent-session.js.map +1 -0
  71. package/dist/state/transports/AppTransport.d.ts +13 -0
  72. package/dist/state/transports/AppTransport.d.ts.map +1 -0
  73. package/dist/state/transports/AppTransport.js +316 -0
  74. package/dist/state/transports/AppTransport.js.map +1 -0
  75. package/dist/state/transports/ProviderTransport.d.ts +12 -0
  76. package/dist/state/transports/ProviderTransport.d.ts.map +1 -0
  77. package/dist/state/transports/ProviderTransport.js +44 -0
  78. package/dist/state/transports/ProviderTransport.js.map +1 -0
  79. package/dist/state/transports/index.d.ts +4 -0
  80. package/dist/state/transports/index.d.ts.map +1 -0
  81. package/dist/state/transports/index.js +4 -0
  82. package/dist/state/transports/index.js.map +1 -0
  83. package/dist/state/transports/proxy-types.d.ts +48 -0
  84. package/dist/state/transports/proxy-types.d.ts.map +1 -0
  85. package/dist/state/transports/proxy-types.js +2 -0
  86. package/dist/state/transports/proxy-types.js.map +1 -0
  87. package/dist/state/transports/types.d.ts +11 -0
  88. package/dist/state/transports/types.d.ts.map +1 -0
  89. package/dist/state/transports/types.js +2 -0
  90. package/dist/state/transports/types.js.map +1 -0
  91. package/dist/state/types.d.ts +15 -0
  92. package/dist/state/types.d.ts.map +1 -0
  93. package/dist/state/types.js +2 -0
  94. package/dist/state/types.js.map +1 -0
  95. package/dist/storage/app-storage.d.ts +26 -0
  96. package/dist/storage/app-storage.d.ts.map +1 -0
  97. package/dist/storage/app-storage.js +44 -0
  98. package/dist/storage/app-storage.js.map +1 -0
  99. package/dist/storage/backends/chrome-storage-backend.d.ts +18 -0
  100. package/dist/storage/backends/chrome-storage-backend.d.ts.map +1 -0
  101. package/dist/storage/backends/chrome-storage-backend.js +67 -0
  102. package/dist/storage/backends/chrome-storage-backend.js.map +1 -0
  103. package/dist/storage/backends/indexeddb-backend.d.ts +20 -0
  104. package/dist/storage/backends/indexeddb-backend.d.ts.map +1 -0
  105. package/dist/storage/backends/indexeddb-backend.js +89 -0
  106. package/dist/storage/backends/indexeddb-backend.js.map +1 -0
  107. package/dist/storage/backends/local-storage-backend.d.ts +18 -0
  108. package/dist/storage/backends/local-storage-backend.d.ts.map +1 -0
  109. package/dist/storage/backends/local-storage-backend.js +69 -0
  110. package/dist/storage/backends/local-storage-backend.js.map +1 -0
  111. package/dist/storage/repositories/provider-keys-repository.d.ts +34 -0
  112. package/dist/storage/repositories/provider-keys-repository.d.ts.map +1 -0
  113. package/dist/storage/repositories/provider-keys-repository.js +50 -0
  114. package/dist/storage/repositories/provider-keys-repository.js.map +1 -0
  115. package/dist/storage/repositories/settings-repository.d.ts +34 -0
  116. package/dist/storage/repositories/settings-repository.d.ts.map +1 -0
  117. package/dist/storage/repositories/settings-repository.js +46 -0
  118. package/dist/storage/repositories/settings-repository.js.map +1 -0
  119. package/dist/storage/types.d.ts +43 -0
  120. package/dist/storage/types.d.ts.map +1 -0
  121. package/dist/storage/types.js +2 -0
  122. package/dist/storage/types.js.map +1 -0
  123. package/dist/tools/artifacts/ArtifactElement.d.ts +10 -0
  124. package/dist/tools/artifacts/ArtifactElement.d.ts.map +1 -0
  125. package/dist/tools/artifacts/ArtifactElement.js +12 -0
  126. package/dist/tools/artifacts/ArtifactElement.js.map +1 -0
  127. package/dist/tools/artifacts/HtmlArtifact.d.ts +30 -0
  128. package/dist/tools/artifacts/HtmlArtifact.d.ts.map +1 -0
  129. package/dist/tools/artifacts/HtmlArtifact.js +217 -0
  130. package/dist/tools/artifacts/HtmlArtifact.js.map +1 -0
  131. package/dist/tools/artifacts/MarkdownArtifact.d.ts +20 -0
  132. package/dist/tools/artifacts/MarkdownArtifact.d.ts.map +1 -0
  133. package/dist/tools/artifacts/MarkdownArtifact.js +84 -0
  134. package/dist/tools/artifacts/MarkdownArtifact.js.map +1 -0
  135. package/dist/tools/artifacts/SvgArtifact.d.ts +19 -0
  136. package/dist/tools/artifacts/SvgArtifact.d.ts.map +1 -0
  137. package/dist/tools/artifacts/SvgArtifact.js +80 -0
  138. package/dist/tools/artifacts/SvgArtifact.js.map +1 -0
  139. package/dist/tools/artifacts/TextArtifact.d.ts +20 -0
  140. package/dist/tools/artifacts/TextArtifact.d.ts.map +1 -0
  141. package/dist/tools/artifacts/TextArtifact.js +147 -0
  142. package/dist/tools/artifacts/TextArtifact.js.map +1 -0
  143. package/dist/tools/artifacts/artifacts.d.ts +67 -0
  144. package/dist/tools/artifacts/artifacts.d.ts.map +1 -0
  145. package/dist/tools/artifacts/artifacts.js +836 -0
  146. package/dist/tools/artifacts/artifacts.js.map +1 -0
  147. package/dist/tools/artifacts/index.d.ts +7 -0
  148. package/dist/tools/artifacts/index.d.ts.map +1 -0
  149. package/dist/tools/artifacts/index.js +7 -0
  150. package/dist/tools/artifacts/index.js.map +1 -0
  151. package/dist/tools/index.d.ts +14 -0
  152. package/dist/tools/index.d.ts.map +1 -0
  153. package/dist/tools/index.js +29 -0
  154. package/dist/tools/index.js.map +1 -0
  155. package/dist/tools/javascript-repl.d.ts +43 -0
  156. package/dist/tools/javascript-repl.d.ts.map +1 -0
  157. package/dist/tools/javascript-repl.js +252 -0
  158. package/dist/tools/javascript-repl.js.map +1 -0
  159. package/dist/tools/renderer-registry.d.ts +11 -0
  160. package/dist/tools/renderer-registry.d.ts.map +1 -0
  161. package/dist/tools/renderer-registry.js +15 -0
  162. package/dist/tools/renderer-registry.js.map +1 -0
  163. package/dist/tools/renderers/BashRenderer.d.ts +12 -0
  164. package/dist/tools/renderers/BashRenderer.d.ts.map +1 -0
  165. package/dist/tools/renderers/BashRenderer.js +35 -0
  166. package/dist/tools/renderers/BashRenderer.js.map +1 -0
  167. package/dist/tools/renderers/CalculateRenderer.d.ts +12 -0
  168. package/dist/tools/renderers/CalculateRenderer.d.ts.map +1 -0
  169. package/dist/tools/renderers/CalculateRenderer.js +38 -0
  170. package/dist/tools/renderers/CalculateRenderer.js.map +1 -0
  171. package/dist/tools/renderers/DefaultRenderer.d.ts +8 -0
  172. package/dist/tools/renderers/DefaultRenderer.d.ts.map +1 -0
  173. package/dist/tools/renderers/DefaultRenderer.js +31 -0
  174. package/dist/tools/renderers/DefaultRenderer.js.map +1 -0
  175. package/dist/tools/renderers/GetCurrentTimeRenderer.d.ts +12 -0
  176. package/dist/tools/renderers/GetCurrentTimeRenderer.d.ts.map +1 -0
  177. package/dist/tools/renderers/GetCurrentTimeRenderer.js +30 -0
  178. package/dist/tools/renderers/GetCurrentTimeRenderer.js.map +1 -0
  179. package/dist/tools/types.d.ts +7 -0
  180. package/dist/tools/types.d.ts.map +1 -0
  181. package/dist/tools/types.js +2 -0
  182. package/dist/tools/types.js.map +1 -0
  183. package/dist/utils/attachment-utils.d.ts +19 -0
  184. package/dist/utils/attachment-utils.d.ts.map +1 -0
  185. package/dist/utils/attachment-utils.js +415 -0
  186. package/dist/utils/attachment-utils.js.map +1 -0
  187. package/dist/utils/auth-token.d.ts +3 -0
  188. package/dist/utils/auth-token.d.ts.map +1 -0
  189. package/dist/utils/auth-token.js +19 -0
  190. package/dist/utils/auth-token.js.map +1 -0
  191. package/dist/utils/format.d.ts +6 -0
  192. package/dist/utils/format.d.ts.map +1 -0
  193. package/dist/utils/format.js +47 -0
  194. package/dist/utils/format.js.map +1 -0
  195. package/dist/utils/i18n.d.ts +111 -0
  196. package/dist/utils/i18n.d.ts.map +1 -0
  197. package/dist/utils/i18n.js +224 -0
  198. package/dist/utils/i18n.js.map +1 -0
  199. package/dist/utils/test-sessions.d.ts +347 -0
  200. package/dist/utils/test-sessions.d.ts.map +1 -0
  201. package/dist/utils/test-sessions.js +2215 -0
  202. package/dist/utils/test-sessions.js.map +1 -0
  203. package/example/README.md +61 -0
  204. package/example/index.html +13 -0
  205. package/example/package-lock.json +1965 -0
  206. package/example/package.json +22 -0
  207. package/example/src/app.css +1 -0
  208. package/example/src/main.ts +57 -0
  209. package/example/src/test-sessions.ts +104 -0
  210. package/example/tsconfig.json +15 -0
  211. package/example/vite.config.ts +6 -0
  212. package/package.json +45 -0
  213. package/src/ChatPanel.ts +214 -0
  214. package/src/app.css +44 -0
  215. package/src/components/AgentInterface.ts +316 -0
  216. package/src/components/AttachmentTile.ts +112 -0
  217. package/src/components/ConsoleBlock.ts +67 -0
  218. package/src/components/Input.ts +112 -0
  219. package/src/components/MessageEditor.ts +272 -0
  220. package/src/components/MessageList.ts +82 -0
  221. package/src/components/Messages.ts +310 -0
  222. package/src/components/ProviderKeyInput.ts +170 -0
  223. package/src/components/SandboxedIframe.ts +525 -0
  224. package/src/components/StreamingMessageContainer.ts +101 -0
  225. package/src/dialogs/ApiKeyPromptDialog.ts +76 -0
  226. package/src/dialogs/AttachmentOverlay.ts +635 -0
  227. package/src/dialogs/ModelSelector.ts +324 -0
  228. package/src/dialogs/SettingsDialog.ts +223 -0
  229. package/src/index.ts +63 -0
  230. package/src/state/agent-session.ts +311 -0
  231. package/src/state/transports/AppTransport.ts +363 -0
  232. package/src/state/transports/ProviderTransport.ts +49 -0
  233. package/src/state/transports/index.ts +3 -0
  234. package/src/state/transports/proxy-types.ts +15 -0
  235. package/src/state/transports/types.ts +16 -0
  236. package/src/state/types.ts +11 -0
  237. package/src/storage/app-storage.ts +53 -0
  238. package/src/storage/backends/chrome-storage-backend.ts +82 -0
  239. package/src/storage/backends/indexeddb-backend.ts +107 -0
  240. package/src/storage/backends/local-storage-backend.ts +74 -0
  241. package/src/storage/repositories/provider-keys-repository.ts +55 -0
  242. package/src/storage/repositories/settings-repository.ts +51 -0
  243. package/src/storage/types.ts +48 -0
  244. package/src/tools/artifacts/ArtifactElement.ts +15 -0
  245. package/src/tools/artifacts/HtmlArtifact.ts +221 -0
  246. package/src/tools/artifacts/MarkdownArtifact.ts +81 -0
  247. package/src/tools/artifacts/SvgArtifact.ts +77 -0
  248. package/src/tools/artifacts/TextArtifact.ts +148 -0
  249. package/src/tools/artifacts/artifacts.ts +888 -0
  250. package/src/tools/artifacts/index.ts +6 -0
  251. package/src/tools/index.ts +35 -0
  252. package/src/tools/javascript-repl.ts +309 -0
  253. package/src/tools/renderer-registry.ts +18 -0
  254. package/src/tools/renderers/BashRenderer.ts +45 -0
  255. package/src/tools/renderers/CalculateRenderer.ts +49 -0
  256. package/src/tools/renderers/DefaultRenderer.ts +36 -0
  257. package/src/tools/renderers/GetCurrentTimeRenderer.ts +39 -0
  258. package/src/tools/types.ts +7 -0
  259. package/src/utils/attachment-utils.ts +472 -0
  260. package/src/utils/auth-token.ts +22 -0
  261. package/src/utils/format.ts +42 -0
  262. package/src/utils/i18n.ts +343 -0
  263. package/src/utils/test-sessions.ts +2247 -0
  264. package/tsconfig.build.json +20 -0
  265. package/tsconfig.json +7 -0
@@ -0,0 +1,170 @@
1
+ import { Badge, Button, html, Input, i18n } from "@mariozechner/mini-lit";
2
+ import { type Context, complete, getModel } from "@mariozechner/pi-ai";
3
+ import { LitElement } from "lit";
4
+ import { customElement, property, state } from "lit/decorators.js";
5
+ import { getAppStorage } from "../storage/app-storage.js";
6
+
7
+ // Test models for each provider
8
+ const TEST_MODELS: Record<string, string> = {
9
+ anthropic: "claude-3-5-haiku-20241022",
10
+ openai: "gpt-4o-mini",
11
+ google: "gemini-2.5-flash",
12
+ groq: "openai/gpt-oss-20b",
13
+ openrouter: "z-ai/glm-4.6",
14
+ cerebras: "gpt-oss-120b",
15
+ xai: "grok-4-fast-non-reasoning",
16
+ zai: "glm-4.5-air",
17
+ };
18
+
19
+ @customElement("provider-key-input")
20
+ export class ProviderKeyInput extends LitElement {
21
+ @property() provider = "";
22
+ @state() private keyInput = "";
23
+ @state() private testing = false;
24
+ @state() private failed = false;
25
+ @state() private hasKey = false;
26
+
27
+ protected createRenderRoot() {
28
+ return this;
29
+ }
30
+
31
+ override async connectedCallback() {
32
+ super.connectedCallback();
33
+ await this.checkKeyStatus();
34
+ }
35
+
36
+ private async checkKeyStatus() {
37
+ try {
38
+ const key = await getAppStorage().providerKeys.getKey(this.provider);
39
+ this.hasKey = !!key;
40
+ } catch (error) {
41
+ console.error("Failed to check key status:", error);
42
+ }
43
+ }
44
+
45
+ private async testApiKey(provider: string, apiKey: string): Promise<boolean> {
46
+ try {
47
+ const modelId = TEST_MODELS[provider];
48
+ if (!modelId) return false;
49
+
50
+ let model = getModel(provider as any, modelId);
51
+ if (!model) return false;
52
+
53
+ // Check if CORS proxy is enabled and apply it
54
+ const proxyEnabled = await getAppStorage().settings.get<boolean>("proxy.enabled");
55
+ const proxyUrl = await getAppStorage().settings.get<string>("proxy.url");
56
+
57
+ if (proxyEnabled && proxyUrl && model.baseUrl) {
58
+ model = {
59
+ ...model,
60
+ baseUrl: `${proxyUrl}/?url=${encodeURIComponent(model.baseUrl)}`,
61
+ };
62
+ }
63
+
64
+ const context: Context = {
65
+ messages: [{ role: "user", content: "Reply with: ok" }],
66
+ };
67
+
68
+ const result = await complete(model, context, {
69
+ apiKey,
70
+ maxTokens: 10,
71
+ } as any);
72
+
73
+ return result.stopReason === "stop";
74
+ } catch (error) {
75
+ console.error(`API key test failed for ${provider}:`, error);
76
+ return false;
77
+ }
78
+ }
79
+
80
+ private async saveKey() {
81
+ if (!this.keyInput) return;
82
+
83
+ this.testing = true;
84
+ this.failed = false;
85
+
86
+ const success = await this.testApiKey(this.provider, this.keyInput);
87
+
88
+ this.testing = false;
89
+
90
+ if (success) {
91
+ try {
92
+ await getAppStorage().providerKeys.setKey(this.provider, this.keyInput);
93
+ this.hasKey = true;
94
+ this.keyInput = "";
95
+ this.requestUpdate();
96
+ } catch (error) {
97
+ console.error("Failed to save API key:", error);
98
+ this.failed = true;
99
+ setTimeout(() => {
100
+ this.failed = false;
101
+ this.requestUpdate();
102
+ }, 5000);
103
+ }
104
+ } else {
105
+ this.failed = true;
106
+ setTimeout(() => {
107
+ this.failed = false;
108
+ this.requestUpdate();
109
+ }, 5000);
110
+ }
111
+ }
112
+
113
+ private async removeKey() {
114
+ try {
115
+ await getAppStorage().providerKeys.removeKey(this.provider);
116
+ this.hasKey = false;
117
+ this.keyInput = "";
118
+ this.requestUpdate();
119
+ } catch (error) {
120
+ console.error("Failed to remove API key:", error);
121
+ }
122
+ }
123
+
124
+ render() {
125
+ return html`
126
+ <div class="space-y-3">
127
+ <div class="flex items-center gap-2">
128
+ <span class="text-sm font-medium capitalize text-foreground">${this.provider}</span>
129
+ ${
130
+ this.testing
131
+ ? Badge({ children: i18n("Testing..."), variant: "secondary" })
132
+ : this.hasKey
133
+ ? html`<span class="text-green-600 dark:text-green-400">✓</span>`
134
+ : ""
135
+ }
136
+ ${this.failed ? Badge({ children: i18n("✗ Invalid"), variant: "destructive" }) : ""}
137
+ </div>
138
+ <div class="flex items-center gap-2">
139
+ ${Input({
140
+ type: "password",
141
+ placeholder: this.hasKey ? "••••••••••••" : i18n("Enter API key"),
142
+ value: this.keyInput,
143
+ onInput: (e: Event) => {
144
+ this.keyInput = (e.target as HTMLInputElement).value;
145
+ this.requestUpdate();
146
+ },
147
+ className: "flex-1",
148
+ })}
149
+ ${
150
+ this.hasKey
151
+ ? Button({
152
+ onClick: () => this.removeKey(),
153
+ variant: "ghost",
154
+ size: "sm",
155
+ children: i18n("Clear"),
156
+ className: "!text-destructive",
157
+ })
158
+ : Button({
159
+ onClick: () => this.saveKey(),
160
+ variant: "default",
161
+ size: "sm",
162
+ disabled: !this.keyInput || this.testing,
163
+ children: i18n("Save"),
164
+ })
165
+ }
166
+ </div>
167
+ </div>
168
+ `;
169
+ }
170
+ }
@@ -0,0 +1,525 @@
1
+ import { LitElement } from "lit";
2
+ import { customElement, property } from "lit/decorators.js";
3
+ import type { Attachment } from "../utils/attachment-utils.js";
4
+
5
+ export interface SandboxFile {
6
+ fileName: string;
7
+ content: string | Uint8Array;
8
+ mimeType: string;
9
+ }
10
+
11
+ export interface SandboxResult {
12
+ success: boolean;
13
+ console: Array<{ type: string; text: string }>;
14
+ files?: SandboxFile[];
15
+ error?: { message: string; stack: string };
16
+ }
17
+
18
+ /**
19
+ * Function that returns the URL to the sandbox HTML file.
20
+ * Used in browser extensions to load sandbox.html via chrome.runtime.getURL().
21
+ */
22
+ export type SandboxUrlProvider = () => string;
23
+
24
+ @customElement("sandbox-iframe")
25
+ export class SandboxIframe extends LitElement {
26
+ private iframe?: HTMLIFrameElement;
27
+
28
+ /**
29
+ * Optional: Provide a function that returns the sandbox HTML URL.
30
+ * If provided, the iframe will use this URL instead of srcdoc.
31
+ * This is required for browser extensions with strict CSP.
32
+ */
33
+ @property({ attribute: false }) sandboxUrlProvider?: SandboxUrlProvider;
34
+
35
+ createRenderRoot() {
36
+ return this;
37
+ }
38
+
39
+ override connectedCallback() {
40
+ super.connectedCallback();
41
+ }
42
+
43
+ override disconnectedCallback() {
44
+ super.disconnectedCallback();
45
+ this.iframe?.remove();
46
+ }
47
+
48
+ /**
49
+ * Load HTML content into sandbox and keep it displayed (for HTML artifacts)
50
+ * @param sandboxId Unique ID
51
+ * @param htmlContent Full HTML content
52
+ * @param attachments Attachments available
53
+ */
54
+ public loadContent(sandboxId: string, htmlContent: string, attachments: Attachment[]): void {
55
+ const completeHtml = this.prepareHtmlDocument(sandboxId, htmlContent, attachments);
56
+
57
+ if (this.sandboxUrlProvider) {
58
+ // Browser extension mode: use sandbox.html with postMessage
59
+ this.loadViaSandboxUrl(sandboxId, completeHtml, attachments);
60
+ } else {
61
+ // Web mode: use srcdoc
62
+ this.loadViaSrcdoc(completeHtml);
63
+ }
64
+ }
65
+
66
+ private loadViaSandboxUrl(sandboxId: string, completeHtml: string, attachments: Attachment[]): void {
67
+ // Wait for sandbox-ready and send content
68
+ const readyHandler = (e: MessageEvent) => {
69
+ if (e.data.type === "sandbox-ready" && e.source === this.iframe?.contentWindow) {
70
+ window.removeEventListener("message", readyHandler);
71
+ this.iframe?.contentWindow?.postMessage(
72
+ {
73
+ type: "sandbox-load",
74
+ sandboxId,
75
+ code: completeHtml,
76
+ attachments,
77
+ },
78
+ "*",
79
+ );
80
+ }
81
+ };
82
+ window.addEventListener("message", readyHandler);
83
+
84
+ // Always recreate iframe to ensure fresh sandbox and sandbox-ready message
85
+ this.iframe?.remove();
86
+ this.iframe = document.createElement("iframe");
87
+ this.iframe.sandbox.add("allow-scripts");
88
+ this.iframe.sandbox.add("allow-modals");
89
+ this.iframe.style.width = "100%";
90
+ this.iframe.style.height = "100%";
91
+ this.iframe.style.border = "none";
92
+
93
+ this.iframe.src = this.sandboxUrlProvider!();
94
+
95
+ this.appendChild(this.iframe);
96
+ }
97
+
98
+ private loadViaSrcdoc(completeHtml: string): void {
99
+ // Always recreate iframe to ensure fresh sandbox
100
+ this.iframe?.remove();
101
+ this.iframe = document.createElement("iframe");
102
+ this.iframe.sandbox.add("allow-scripts");
103
+ this.iframe.sandbox.add("allow-modals");
104
+ this.iframe.style.width = "100%";
105
+ this.iframe.style.height = "100%";
106
+ this.iframe.style.border = "none";
107
+
108
+ // Set content directly via srcdoc (no CSP restrictions in web apps)
109
+ this.iframe.srcdoc = completeHtml;
110
+
111
+ this.appendChild(this.iframe);
112
+ }
113
+
114
+ /**
115
+ * Execute code in sandbox
116
+ * @param sandboxId Unique ID for this execution
117
+ * @param code User code (plain JS for REPL, or full HTML for artifacts)
118
+ * @param attachments Attachments available to the code
119
+ * @param signal Abort signal
120
+ * @returns Promise resolving to execution result
121
+ */
122
+ public async execute(
123
+ sandboxId: string,
124
+ code: string,
125
+ attachments: Attachment[],
126
+ signal?: AbortSignal,
127
+ ): Promise<SandboxResult> {
128
+ if (signal?.aborted) {
129
+ throw new Error("Execution aborted");
130
+ }
131
+
132
+ // Prepare the complete HTML document with runtime + user code
133
+ const completeHtml = this.prepareHtmlDocument(sandboxId, code, attachments);
134
+
135
+ // Wait for execution to complete
136
+ return new Promise((resolve, reject) => {
137
+ const logs: Array<{ type: string; text: string }> = [];
138
+ const files: SandboxFile[] = [];
139
+ let completed = false;
140
+
141
+ const messageHandler = (e: MessageEvent) => {
142
+ // Ignore messages not for this sandbox
143
+ if (e.data.sandboxId !== sandboxId) return;
144
+
145
+ if (e.data.type === "console") {
146
+ logs.push({
147
+ type: e.data.method === "error" ? "error" : "log",
148
+ text: e.data.text,
149
+ });
150
+ } else if (e.data.type === "file-returned") {
151
+ files.push({
152
+ fileName: e.data.fileName,
153
+ content: e.data.content,
154
+ mimeType: e.data.mimeType,
155
+ });
156
+ } else if (e.data.type === "execution-complete") {
157
+ completed = true;
158
+ cleanup();
159
+ resolve({
160
+ success: true,
161
+ console: logs,
162
+ files: files,
163
+ });
164
+ } else if (e.data.type === "execution-error") {
165
+ completed = true;
166
+ cleanup();
167
+ resolve({
168
+ success: false,
169
+ console: logs,
170
+ error: e.data.error,
171
+ files,
172
+ });
173
+ }
174
+ };
175
+
176
+ const abortHandler = () => {
177
+ if (!completed) {
178
+ cleanup();
179
+ reject(new Error("Execution aborted"));
180
+ }
181
+ };
182
+
183
+ let readyHandler: ((e: MessageEvent) => void) | undefined;
184
+
185
+ const cleanup = () => {
186
+ window.removeEventListener("message", messageHandler);
187
+ signal?.removeEventListener("abort", abortHandler);
188
+ if (readyHandler) {
189
+ window.removeEventListener("message", readyHandler);
190
+ }
191
+ clearTimeout(timeoutId);
192
+ };
193
+
194
+ // Set up listeners BEFORE creating iframe
195
+ window.addEventListener("message", messageHandler);
196
+ signal?.addEventListener("abort", abortHandler);
197
+
198
+ // Timeout after 30 seconds
199
+ const timeoutId = setTimeout(() => {
200
+ if (!completed) {
201
+ cleanup();
202
+ resolve({
203
+ success: false,
204
+ error: { message: "Execution timeout (30s)", stack: "" },
205
+ console: logs,
206
+ files,
207
+ });
208
+ }
209
+ }, 30000);
210
+
211
+ if (this.sandboxUrlProvider) {
212
+ // Browser extension mode: wait for sandbox-ready and send content
213
+ readyHandler = (e: MessageEvent) => {
214
+ if (e.data.type === "sandbox-ready" && e.source === this.iframe?.contentWindow) {
215
+ window.removeEventListener("message", readyHandler!);
216
+ // Send the complete HTML
217
+ this.iframe?.contentWindow?.postMessage(
218
+ {
219
+ type: "sandbox-load",
220
+ sandboxId,
221
+ code: completeHtml,
222
+ attachments,
223
+ },
224
+ "*",
225
+ );
226
+ }
227
+ };
228
+ window.addEventListener("message", readyHandler);
229
+
230
+ // Create iframe AFTER all listeners are set up
231
+ this.iframe?.remove();
232
+ this.iframe = document.createElement("iframe");
233
+ this.iframe.sandbox.add("allow-scripts");
234
+ this.iframe.sandbox.add("allow-modals");
235
+ this.iframe.style.width = "100%";
236
+ this.iframe.style.height = "100%";
237
+ this.iframe.style.border = "none";
238
+
239
+ this.iframe.src = this.sandboxUrlProvider();
240
+
241
+ this.appendChild(this.iframe);
242
+ } else {
243
+ // Web mode: use srcdoc
244
+ this.iframe?.remove();
245
+ this.iframe = document.createElement("iframe");
246
+ this.iframe.sandbox.add("allow-scripts");
247
+ this.iframe.sandbox.add("allow-modals");
248
+ this.iframe.style.width = "100%";
249
+ this.iframe.style.height = "100%";
250
+ this.iframe.style.border = "none";
251
+
252
+ // Set content via srcdoc BEFORE appending to DOM
253
+ this.iframe.srcdoc = completeHtml;
254
+
255
+ this.appendChild(this.iframe);
256
+ }
257
+ });
258
+ }
259
+
260
+ /**
261
+ * Prepare complete HTML document with runtime + user code
262
+ */
263
+ private prepareHtmlDocument(sandboxId: string, userCode: string, attachments: Attachment[]): string {
264
+ // Runtime script that will be injected
265
+ const runtime = this.getRuntimeScript(sandboxId, attachments);
266
+
267
+ // Check if user provided full HTML
268
+ const hasHtmlTag = /<html[^>]*>/i.test(userCode);
269
+
270
+ if (hasHtmlTag) {
271
+ // HTML Artifact - inject runtime into existing HTML
272
+ const headMatch = userCode.match(/<head[^>]*>/i);
273
+ if (headMatch) {
274
+ const index = headMatch.index! + headMatch[0].length;
275
+ return userCode.slice(0, index) + runtime + userCode.slice(index);
276
+ }
277
+
278
+ const htmlMatch = userCode.match(/<html[^>]*>/i);
279
+ if (htmlMatch) {
280
+ const index = htmlMatch.index! + htmlMatch[0].length;
281
+ return userCode.slice(0, index) + runtime + userCode.slice(index);
282
+ }
283
+
284
+ // Fallback: prepend runtime
285
+ return runtime + userCode;
286
+ } else {
287
+ // REPL - wrap code in HTML with runtime and call complete() when done
288
+ return `<!DOCTYPE html>
289
+ <html>
290
+ <head>
291
+ ${runtime}
292
+ </head>
293
+ <body>
294
+ <script type="module">
295
+ (async () => {
296
+ try {
297
+ ${userCode}
298
+ window.complete();
299
+ } catch (error) {
300
+ console.error(error?.stack || error?.message || String(error));
301
+ window.complete({
302
+ message: error?.message || String(error),
303
+ stack: error?.stack || new Error().stack
304
+ });
305
+ }
306
+ })();
307
+ </script>
308
+ </body>
309
+ </html>`;
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Get the runtime script that captures console, provides helpers, etc.
315
+ */
316
+ private getRuntimeScript(sandboxId: string, attachments: Attachment[]): string {
317
+ // Convert attachments to serializable format
318
+ const attachmentsData = attachments.map((a) => ({
319
+ id: a.id,
320
+ fileName: a.fileName,
321
+ mimeType: a.mimeType,
322
+ size: a.size,
323
+ content: a.content,
324
+ extractedText: a.extractedText,
325
+ }));
326
+
327
+ // Runtime function that will run in the sandbox (NO parameters - values injected before function)
328
+ const runtimeFunc = () => {
329
+ // Helper functions
330
+ (window as any).listFiles = () =>
331
+ (attachments || []).map((a: any) => ({
332
+ id: a.id,
333
+ fileName: a.fileName,
334
+ mimeType: a.mimeType,
335
+ size: a.size,
336
+ }));
337
+
338
+ (window as any).readTextFile = (attachmentId: string) => {
339
+ const a = (attachments || []).find((x: any) => x.id === attachmentId);
340
+ if (!a) throw new Error("Attachment not found: " + attachmentId);
341
+ if (a.extractedText) return a.extractedText;
342
+ try {
343
+ return atob(a.content);
344
+ } catch {
345
+ throw new Error("Failed to decode text content for: " + attachmentId);
346
+ }
347
+ };
348
+
349
+ (window as any).readBinaryFile = (attachmentId: string) => {
350
+ const a = (attachments || []).find((x: any) => x.id === attachmentId);
351
+ if (!a) throw new Error("Attachment not found: " + attachmentId);
352
+ const bin = atob(a.content);
353
+ const bytes = new Uint8Array(bin.length);
354
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
355
+ return bytes;
356
+ };
357
+
358
+ (window as any).returnFile = async (fileName: string, content: any, mimeType?: string) => {
359
+ let finalContent: any, finalMimeType: string;
360
+
361
+ if (content instanceof Blob) {
362
+ const arrayBuffer = await content.arrayBuffer();
363
+ finalContent = new Uint8Array(arrayBuffer);
364
+ finalMimeType = mimeType || content.type || "application/octet-stream";
365
+ if (!mimeType && !content.type) {
366
+ throw new Error(
367
+ "returnFile: MIME type is required for Blob content. Please provide a mimeType parameter (e.g., 'image/png').",
368
+ );
369
+ }
370
+ } else if (content instanceof Uint8Array) {
371
+ finalContent = content;
372
+ if (!mimeType) {
373
+ throw new Error(
374
+ "returnFile: MIME type is required for Uint8Array content. Please provide a mimeType parameter (e.g., 'image/png').",
375
+ );
376
+ }
377
+ finalMimeType = mimeType;
378
+ } else if (typeof content === "string") {
379
+ finalContent = content;
380
+ finalMimeType = mimeType || "text/plain";
381
+ } else {
382
+ finalContent = JSON.stringify(content, null, 2);
383
+ finalMimeType = mimeType || "application/json";
384
+ }
385
+
386
+ window.parent.postMessage(
387
+ {
388
+ type: "file-returned",
389
+ sandboxId,
390
+ fileName,
391
+ content: finalContent,
392
+ mimeType: finalMimeType,
393
+ },
394
+ "*",
395
+ );
396
+ };
397
+
398
+ // Console capture
399
+ const originalConsole = {
400
+ log: console.log,
401
+ error: console.error,
402
+ warn: console.warn,
403
+ info: console.info,
404
+ };
405
+
406
+ ["log", "error", "warn", "info"].forEach((method) => {
407
+ (console as any)[method] = (...args: any[]) => {
408
+ const text = args
409
+ .map((arg) => {
410
+ try {
411
+ return typeof arg === "object" ? JSON.stringify(arg) : String(arg);
412
+ } catch {
413
+ return String(arg);
414
+ }
415
+ })
416
+ .join(" ");
417
+
418
+ window.parent.postMessage(
419
+ {
420
+ type: "console",
421
+ sandboxId,
422
+ method,
423
+ text,
424
+ },
425
+ "*",
426
+ );
427
+
428
+ (originalConsole as any)[method].apply(console, args);
429
+ };
430
+ });
431
+
432
+ // Track errors for HTML artifacts
433
+ let lastError: { message: string; stack: string } | null = null;
434
+
435
+ // Error handlers
436
+ window.addEventListener("error", (e) => {
437
+ const text =
438
+ (e.error?.stack || e.message || String(e)) + " at line " + (e.lineno || "?") + ":" + (e.colno || "?");
439
+
440
+ // Store the error
441
+ lastError = {
442
+ message: e.error?.message || e.message || String(e),
443
+ stack: e.error?.stack || text,
444
+ };
445
+
446
+ window.parent.postMessage(
447
+ {
448
+ type: "console",
449
+ sandboxId,
450
+ method: "error",
451
+ text,
452
+ },
453
+ "*",
454
+ );
455
+ });
456
+
457
+ window.addEventListener("unhandledrejection", (e) => {
458
+ const text = "Unhandled promise rejection: " + (e.reason?.message || e.reason || "Unknown error");
459
+
460
+ // Store the error
461
+ lastError = {
462
+ message: e.reason?.message || String(e.reason) || "Unhandled promise rejection",
463
+ stack: e.reason?.stack || text,
464
+ };
465
+
466
+ window.parent.postMessage(
467
+ {
468
+ type: "console",
469
+ sandboxId,
470
+ method: "error",
471
+ text,
472
+ },
473
+ "*",
474
+ );
475
+ });
476
+
477
+ // Expose complete() method for user code to call
478
+ let completionSent = false;
479
+ (window as any).complete = (error?: { message: string; stack: string }) => {
480
+ if (completionSent) return;
481
+ completionSent = true;
482
+
483
+ // Use provided error or last caught error
484
+ const finalError = error || lastError;
485
+
486
+ if (finalError) {
487
+ window.parent.postMessage(
488
+ {
489
+ type: "execution-error",
490
+ sandboxId,
491
+ error: finalError,
492
+ },
493
+ "*",
494
+ );
495
+ } else {
496
+ window.parent.postMessage(
497
+ {
498
+ type: "execution-complete",
499
+ sandboxId,
500
+ },
501
+ "*",
502
+ );
503
+ }
504
+ };
505
+
506
+ // Fallback timeout for HTML artifacts that don't call complete()
507
+ if (document.readyState === "complete" || document.readyState === "interactive") {
508
+ setTimeout(() => (window as any).complete(), 2000);
509
+ } else {
510
+ window.addEventListener("load", () => {
511
+ setTimeout(() => (window as any).complete(), 2000);
512
+ });
513
+ }
514
+ };
515
+
516
+ // Prepend the const declarations, then the function
517
+ return (
518
+ `<script>\n` +
519
+ `window.sandboxId = ${JSON.stringify(sandboxId)};\n` +
520
+ `window.attachments = ${JSON.stringify(attachmentsData)};\n` +
521
+ `(${runtimeFunc.toString()})();\n` +
522
+ `</script>`
523
+ );
524
+ }
525
+ }