@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,888 @@
1
+ import { Button, Diff, icon } from "@mariozechner/mini-lit";
2
+ import { type AgentTool, type Message, StringEnum, type ToolCall, type ToolResultMessage } from "@mariozechner/pi-ai";
3
+ import { type Static, Type } from "@sinclair/typebox";
4
+ import { html, LitElement, type TemplateResult } from "lit";
5
+ import { customElement, property, state } from "lit/decorators.js";
6
+ import { createRef, type Ref, ref } from "lit/directives/ref.js";
7
+ import { X } from "lucide";
8
+ import type { Attachment } from "../../utils/attachment-utils.js";
9
+ import { i18n } from "../../utils/i18n.js";
10
+ import type { ToolRenderer } from "../types.js";
11
+ import type { ArtifactElement } from "./ArtifactElement.js";
12
+ import { HtmlArtifact } from "./HtmlArtifact.js";
13
+ import { MarkdownArtifact } from "./MarkdownArtifact.js";
14
+ import { SvgArtifact } from "./SvgArtifact.js";
15
+ import { TextArtifact } from "./TextArtifact.js";
16
+ import "@mariozechner/mini-lit/dist/MarkdownBlock.js";
17
+ import "@mariozechner/mini-lit/dist/CodeBlock.js";
18
+
19
+ // Simple artifact model
20
+ export interface Artifact {
21
+ filename: string;
22
+ title: string;
23
+ content: string;
24
+ createdAt: Date;
25
+ updatedAt: Date;
26
+ }
27
+
28
+ // JSON-schema friendly parameters object (LLM-facing)
29
+ const artifactsParamsSchema = Type.Object({
30
+ command: StringEnum(["create", "update", "rewrite", "get", "delete", "logs"], {
31
+ description: "The operation to perform",
32
+ }),
33
+ filename: Type.String({ description: "Filename including extension (e.g., 'index.html', 'script.js')" }),
34
+ title: Type.Optional(Type.String({ description: "Display title for the tab (defaults to filename)" })),
35
+ content: Type.Optional(Type.String({ description: "File content" })),
36
+ old_str: Type.Optional(Type.String({ description: "String to replace (for update command)" })),
37
+ new_str: Type.Optional(Type.String({ description: "Replacement string (for update command)" })),
38
+ });
39
+ export type ArtifactsParams = Static<typeof artifactsParamsSchema>;
40
+
41
+ // Minimal helper to render plain text outputs consistently
42
+ function plainOutput(text: string): TemplateResult {
43
+ return html`<div class="text-xs text-muted-foreground whitespace-pre-wrap font-mono">${text}</div>`;
44
+ }
45
+
46
+ @customElement("artifacts-panel")
47
+ export class ArtifactsPanel extends LitElement implements ToolRenderer<ArtifactsParams, undefined> {
48
+ @state() private _artifacts = new Map<string, Artifact>();
49
+ @state() private _activeFilename: string | null = null;
50
+
51
+ // Programmatically managed artifact elements
52
+ private artifactElements = new Map<string, ArtifactElement>();
53
+ private contentRef: Ref<HTMLDivElement> = createRef();
54
+
55
+ // External provider for attachments (decouples panel from AgentInterface)
56
+ @property({ attribute: false }) attachmentsProvider?: () => Attachment[];
57
+ // Sandbox URL provider for browser extensions (optional)
58
+ @property({ attribute: false }) sandboxUrlProvider?: () => string;
59
+ // Callbacks
60
+ @property({ attribute: false }) onArtifactsChange?: () => void;
61
+ @property({ attribute: false }) onClose?: () => void;
62
+ @property({ attribute: false }) onOpen?: () => void;
63
+ // Collapsed mode: hides panel content but can show a floating reopen pill
64
+ @property({ type: Boolean }) collapsed = false;
65
+ // Overlay mode: when true, panel renders full-screen overlay (mobile)
66
+ @property({ type: Boolean }) overlay = false;
67
+
68
+ // Public getter for artifacts
69
+ get artifacts() {
70
+ return this._artifacts;
71
+ }
72
+
73
+ protected override createRenderRoot(): HTMLElement | DocumentFragment {
74
+ return this; // light DOM for shared styles
75
+ }
76
+
77
+ override connectedCallback(): void {
78
+ super.connectedCallback();
79
+ this.style.display = "block";
80
+ this.style.height = "100%";
81
+ // Reattach existing artifact elements when panel is re-inserted into the DOM
82
+ requestAnimationFrame(() => {
83
+ const container = this.contentRef.value;
84
+ if (!container) return;
85
+ // Ensure we have an active filename
86
+ if (!this._activeFilename && this._artifacts.size > 0) {
87
+ this._activeFilename = Array.from(this._artifacts.keys())[0];
88
+ }
89
+ this.artifactElements.forEach((element, name) => {
90
+ if (!element.parentElement) container.appendChild(element);
91
+ element.style.display = name === this._activeFilename ? "block" : "none";
92
+ });
93
+ });
94
+ }
95
+
96
+ override disconnectedCallback() {
97
+ super.disconnectedCallback();
98
+ // Do not tear down artifact elements; keep them to restore on next mount
99
+ }
100
+
101
+ // Helper to determine file type from extension
102
+ private getFileType(filename: string): "html" | "svg" | "markdown" | "text" {
103
+ const ext = filename.split(".").pop()?.toLowerCase();
104
+ if (ext === "html") return "html";
105
+ if (ext === "svg") return "svg";
106
+ if (ext === "md" || ext === "markdown") return "markdown";
107
+ return "text";
108
+ }
109
+
110
+ // Helper to determine language for syntax highlighting
111
+ private getLanguageFromFilename(filename?: string): string {
112
+ if (!filename) return "text";
113
+ const ext = filename.split(".").pop()?.toLowerCase();
114
+ const languageMap: Record<string, string> = {
115
+ js: "javascript",
116
+ jsx: "javascript",
117
+ ts: "typescript",
118
+ tsx: "typescript",
119
+ html: "html",
120
+ css: "css",
121
+ scss: "scss",
122
+ json: "json",
123
+ py: "python",
124
+ md: "markdown",
125
+ svg: "xml",
126
+ xml: "xml",
127
+ yaml: "yaml",
128
+ yml: "yaml",
129
+ sh: "bash",
130
+ bash: "bash",
131
+ sql: "sql",
132
+ java: "java",
133
+ c: "c",
134
+ cpp: "cpp",
135
+ cs: "csharp",
136
+ go: "go",
137
+ rs: "rust",
138
+ php: "php",
139
+ rb: "ruby",
140
+ swift: "swift",
141
+ kt: "kotlin",
142
+ r: "r",
143
+ };
144
+ return languageMap[ext || ""] || "text";
145
+ }
146
+
147
+ // Get or create artifact element
148
+ private getOrCreateArtifactElement(filename: string, content: string, title: string): ArtifactElement {
149
+ let element = this.artifactElements.get(filename);
150
+
151
+ if (!element) {
152
+ const type = this.getFileType(filename);
153
+ if (type === "html") {
154
+ element = new HtmlArtifact();
155
+ (element as HtmlArtifact).attachments = this.attachmentsProvider?.() || [];
156
+ if (this.sandboxUrlProvider) {
157
+ (element as HtmlArtifact).sandboxUrlProvider = this.sandboxUrlProvider;
158
+ }
159
+ } else if (type === "svg") {
160
+ element = new SvgArtifact();
161
+ } else if (type === "markdown") {
162
+ element = new MarkdownArtifact();
163
+ } else {
164
+ element = new TextArtifact();
165
+ }
166
+ element.filename = filename;
167
+ element.displayTitle = title;
168
+ element.content = content;
169
+ element.style.display = "none";
170
+ element.style.height = "100%";
171
+
172
+ // Store element
173
+ this.artifactElements.set(filename, element);
174
+
175
+ // Add to DOM - try immediately if container exists, otherwise schedule
176
+ const newElement = element;
177
+ if (this.contentRef.value) {
178
+ this.contentRef.value.appendChild(newElement);
179
+ } else {
180
+ requestAnimationFrame(() => {
181
+ if (this.contentRef.value && !newElement.parentElement) {
182
+ this.contentRef.value.appendChild(newElement);
183
+ }
184
+ });
185
+ }
186
+ } else {
187
+ // Just update content
188
+ element.content = content;
189
+ element.displayTitle = title;
190
+ if (element instanceof HtmlArtifact) {
191
+ element.attachments = this.attachmentsProvider?.() || [];
192
+ }
193
+ }
194
+
195
+ return element;
196
+ }
197
+
198
+ // Show/hide artifact elements
199
+ private showArtifact(filename: string) {
200
+ // Ensure the active element is in the DOM
201
+ requestAnimationFrame(() => {
202
+ this.artifactElements.forEach((element, name) => {
203
+ if (this.contentRef.value && !element.parentElement) {
204
+ this.contentRef.value.appendChild(element);
205
+ }
206
+ element.style.display = name === filename ? "block" : "none";
207
+ });
208
+ });
209
+ this._activeFilename = filename;
210
+ this.requestUpdate(); // Only for tab bar update
211
+ }
212
+
213
+ // Open panel and focus an artifact tab by filename
214
+ private openArtifact(filename: string) {
215
+ if (this._artifacts.has(filename)) {
216
+ this.showArtifact(filename);
217
+ // Ask host to open panel (AgentInterface demo listens to onOpen)
218
+ this.onOpen?.();
219
+ }
220
+ }
221
+
222
+ // Build the AgentTool (no details payload; return only output strings)
223
+ public get tool(): AgentTool<typeof artifactsParamsSchema, undefined> {
224
+ return {
225
+ label: "Artifacts",
226
+ name: "artifacts",
227
+ description: `Creates and manages file artifacts. Each artifact is a file with a filename and content.
228
+
229
+ IMPORTANT: Always prefer updating existing files over creating new ones. Check available files first.
230
+
231
+ Commands:
232
+ 1. create: Create a new file
233
+ - filename: Name with extension (required, e.g., 'index.html', 'script.js', 'README.md')
234
+ - title: Display name for the tab (optional, defaults to filename)
235
+ - content: File content (required)
236
+
237
+ 2. update: Update part of an existing file
238
+ - filename: File to update (required)
239
+ - old_str: Exact string to replace (required)
240
+ - new_str: Replacement string (required)
241
+
242
+ 3. rewrite: Completely replace a file's content
243
+ - filename: File to rewrite (required)
244
+ - content: New content (required)
245
+ - title: Optionally update display title
246
+
247
+ 4. get: Retrieve the full content of a file
248
+ - filename: File to retrieve (required)
249
+ - Returns the complete file content
250
+
251
+ 5. delete: Delete a file
252
+ - filename: File to delete (required)
253
+
254
+ 6. logs: Get console logs and errors (HTML files only)
255
+ - filename: HTML file to get logs for (required)
256
+ - Returns all console output and runtime errors
257
+
258
+ For text/html artifacts with attachments:
259
+ - HTML artifacts automatically have access to user attachments via JavaScript
260
+ - Available global functions in HTML artifacts:
261
+ * listFiles() - Returns array of {id, fileName, mimeType, size} for all attachments
262
+ * readTextFile(attachmentId) - Returns text content of attachment (for CSV, JSON, text files)
263
+ * readBinaryFile(attachmentId) - Returns Uint8Array of binary data (for images, Excel, etc.)
264
+ - Example HTML artifact that processes a CSV attachment:
265
+ <script>
266
+ // List available files
267
+ const files = listFiles();
268
+ console.log('Available files:', files);
269
+
270
+ // Find CSV file
271
+ const csvFile = files.find(f => f.mimeType === 'text/csv');
272
+ if (csvFile) {
273
+ const csvContent = readTextFile(csvFile.id);
274
+ // Process CSV data...
275
+ }
276
+
277
+ // Display image
278
+ const imageFile = files.find(f => f.mimeType.startsWith('image/'));
279
+ if (imageFile) {
280
+ const bytes = readBinaryFile(imageFile.id);
281
+ const blob = new Blob([bytes], {type: imageFile.mimeType});
282
+ const url = URL.createObjectURL(blob);
283
+ document.body.innerHTML = '<img src="' + url + '">';
284
+ }
285
+ </script>
286
+
287
+ For text/html artifacts:
288
+ - Must be a single self-contained file
289
+ - External scripts: Use CDNs like https://esm.sh, https://unpkg.com, or https://cdnjs.cloudflare.com
290
+ - Preferred: Use https://esm.sh for npm packages (e.g., https://esm.sh/three for Three.js)
291
+ - For ES modules, use: <script type="module">import * as THREE from 'https://esm.sh/three';</script>
292
+ - For Three.js specifically: import from 'https://esm.sh/three' or 'https://esm.sh/three@0.160.0'
293
+ - For addons: import from 'https://esm.sh/three/examples/jsm/controls/OrbitControls.js'
294
+ - No localStorage/sessionStorage - use in-memory variables only
295
+ - CSS should be included inline
296
+ - CRITICAL REMINDER FOR HTML ARTIFACTS:
297
+ - ALWAYS set a background color inline in <style> or directly on body element
298
+ - Failure to set a background color is a COMPLIANCE ERROR
299
+ - Background color MUST be explicitly defined to ensure visibility and proper rendering
300
+ - Can embed base64 images directly in img tags
301
+ - Ensure the layout is responsive as the iframe might be resized
302
+ - Note: Network errors (404s) for external scripts may not be captured in logs due to browser security
303
+
304
+ For application/vnd.ant.code artifacts:
305
+ - Include the language parameter for syntax highlighting
306
+ - Supports all major programming languages
307
+
308
+ For text/markdown:
309
+ - Standard markdown syntax
310
+ - Will be rendered with full formatting
311
+ - Can include base64 images using markdown syntax
312
+
313
+ For image/svg+xml:
314
+ - Complete SVG markup
315
+ - Will be rendered inline
316
+ - Can embed raster images as base64 in SVG
317
+
318
+ CRITICAL REMINDER FOR ALL ARTIFACTS:
319
+ - Prefer to update existing files rather than creating new ones
320
+ - Keep filenames consistent and descriptive
321
+ - Use appropriate file extensions
322
+ - Ensure HTML artifacts have a defined background color
323
+ `,
324
+ parameters: artifactsParamsSchema,
325
+ // Execute mutates our local store and returns a plain output
326
+ execute: async (_toolCallId: string, args: Static<typeof artifactsParamsSchema>, _signal?: AbortSignal) => {
327
+ const output = await this.executeCommand(args);
328
+ return { output, details: undefined };
329
+ },
330
+ };
331
+ }
332
+
333
+ // ToolRenderer implementation
334
+ renderParams(params: ArtifactsParams, isStreaming?: boolean): TemplateResult {
335
+ if (isStreaming && !params.command) {
336
+ return html`<div class="text-sm text-muted-foreground">${i18n("Processing artifact...")}</div>`;
337
+ }
338
+
339
+ let commandLabel = i18n("Processing");
340
+ if (params.command) {
341
+ switch (params.command) {
342
+ case "create":
343
+ commandLabel = i18n("Create");
344
+ break;
345
+ case "update":
346
+ commandLabel = i18n("Update");
347
+ break;
348
+ case "rewrite":
349
+ commandLabel = i18n("Rewrite");
350
+ break;
351
+ case "get":
352
+ commandLabel = i18n("Get");
353
+ break;
354
+ case "delete":
355
+ commandLabel = i18n("Delete");
356
+ break;
357
+ case "logs":
358
+ commandLabel = i18n("Get logs");
359
+ break;
360
+ default:
361
+ commandLabel = params.command.charAt(0).toUpperCase() + params.command.slice(1);
362
+ }
363
+ }
364
+ const filename = params.filename || "";
365
+
366
+ switch (params.command) {
367
+ case "create":
368
+ return html`
369
+ <div
370
+ class="text-sm cursor-pointer hover:bg-muted/50 rounded-sm px-2 py-1"
371
+ @click=${() => this.openArtifact(params.filename)}
372
+ >
373
+ <div>
374
+ <span class="font-medium">${i18n("Create")}</span>
375
+ <span class="text-muted-foreground ml-1">${filename}</span>
376
+ </div>
377
+ ${
378
+ params.content
379
+ ? html`<code-block
380
+ .code=${params.content}
381
+ language=${this.getLanguageFromFilename(params.filename)}
382
+ class="mt-2"
383
+ ></code-block>`
384
+ : ""
385
+ }
386
+ </div>
387
+ `;
388
+ case "update":
389
+ return html`
390
+ <div
391
+ class="text-sm cursor-pointer hover:bg-muted/50 rounded-sm px-2 py-1"
392
+ @click=${() => this.openArtifact(params.filename)}
393
+ >
394
+ <div>
395
+ <span class="font-medium">${i18n("Update")}</span>
396
+ <span class="text-muted-foreground ml-1">${filename}</span>
397
+ </div>
398
+ ${
399
+ params.old_str !== undefined && params.new_str !== undefined
400
+ ? Diff({ oldText: params.old_str, newText: params.new_str, className: "mt-2" })
401
+ : ""
402
+ }
403
+ </div>
404
+ `;
405
+ case "rewrite":
406
+ return html`
407
+ <div
408
+ class="text-sm cursor-pointer hover:bg-muted/50 rounded-sm px-2 py-1"
409
+ @click=${() => this.openArtifact(params.filename)}
410
+ >
411
+ <div>
412
+ <span class="font-medium">${i18n("Rewrite")}</span>
413
+ <span class="text-muted-foreground ml-1">${filename}</span>
414
+ </div>
415
+ ${
416
+ params.content
417
+ ? html`<code-block
418
+ .code=${params.content}
419
+ language=${this.getLanguageFromFilename(params.filename)}
420
+ class="mt-2"
421
+ ></code-block>`
422
+ : ""
423
+ }
424
+ </div>
425
+ `;
426
+ case "get":
427
+ return html`
428
+ <div
429
+ class="text-sm cursor-pointer hover:bg-muted/50 rounded-sm px-2 py-1"
430
+ @click=${() => this.openArtifact(params.filename)}
431
+ >
432
+ <span class="font-medium">${i18n("Get")}</span>
433
+ <span class="text-muted-foreground ml-1">${filename}</span>
434
+ </div>
435
+ `;
436
+ case "delete":
437
+ return html`
438
+ <div
439
+ class="text-sm cursor-pointer hover:bg-muted/50 rounded-sm px-2 py-1"
440
+ @click=${() => this.openArtifact(params.filename)}
441
+ >
442
+ <span class="font-medium">${i18n("Delete")}</span>
443
+ <span class="text-muted-foreground ml-1">${filename}</span>
444
+ </div>
445
+ `;
446
+ case "logs":
447
+ return html`
448
+ <div
449
+ class="text-sm cursor-pointer hover:bg-muted/50 rounded-sm px-2 py-1"
450
+ @click=${() => this.openArtifact(params.filename)}
451
+ >
452
+ <span class="font-medium">${i18n("Get logs")}</span>
453
+ <span class="text-muted-foreground ml-1">${filename}</span>
454
+ </div>
455
+ `;
456
+ default:
457
+ // Fallback for any command not yet handled during streaming
458
+ return html`
459
+ <div
460
+ class="text-sm cursor-pointer hover:bg-muted/50 rounded-sm px-2 py-1"
461
+ @click=${() => this.openArtifact(params.filename)}
462
+ >
463
+ <span class="font-medium">${commandLabel}</span>
464
+ <span class="text-muted-foreground ml-1">${filename}</span>
465
+ </div>
466
+ `;
467
+ }
468
+ }
469
+
470
+ renderResult(params: ArtifactsParams, result: ToolResultMessage<undefined>): TemplateResult {
471
+ // Make result clickable to focus the referenced file when applicable
472
+ const content = result.output || i18n("(no output)");
473
+ return html`
474
+ <div class="cursor-pointer hover:bg-muted/50 rounded-sm px-2 py-1" @click=${() => this.openArtifact(params.filename)}>
475
+ ${plainOutput(content)}
476
+ </div>
477
+ `;
478
+ }
479
+
480
+ // Re-apply artifacts by scanning a message list (optional utility)
481
+ public async reconstructFromMessages(messages: Array<Message | { role: "aborted" }>): Promise<void> {
482
+ const toolCalls = new Map<string, ToolCall>();
483
+ const artifactToolName = "artifacts";
484
+
485
+ // 1) Collect tool calls from assistant messages
486
+ for (const message of messages) {
487
+ if (message.role === "assistant") {
488
+ for (const block of message.content) {
489
+ if (block.type === "toolCall" && block.name === artifactToolName) {
490
+ toolCalls.set(block.id, block);
491
+ }
492
+ }
493
+ }
494
+ }
495
+
496
+ // 2) Build an ordered list of successful artifact operations
497
+ const operations: Array<ArtifactsParams> = [];
498
+ for (const m of messages) {
499
+ if ((m as any).role === "toolResult" && (m as any).toolName === artifactToolName && !(m as any).isError) {
500
+ const toolCallId = (m as any).toolCallId as string;
501
+ const call = toolCalls.get(toolCallId);
502
+ if (!call) continue;
503
+ const params = call.arguments as ArtifactsParams;
504
+ if (params.command === "get" || params.command === "logs") continue; // no state change
505
+ operations.push(params);
506
+ }
507
+ }
508
+
509
+ // 3) Compute final state per filename by simulating operations in-memory
510
+ type FinalArtifact = { title: string; content: string };
511
+ const finalArtifacts = new Map<string, FinalArtifact>();
512
+ for (const op of operations) {
513
+ const filename = op.filename;
514
+ switch (op.command) {
515
+ case "create": {
516
+ if (op.content) {
517
+ finalArtifacts.set(filename, { title: op.title || filename, content: op.content });
518
+ }
519
+ break;
520
+ }
521
+ case "rewrite": {
522
+ if (op.content) {
523
+ // If file didn't exist earlier but rewrite succeeded, treat as fresh content
524
+ const existing = finalArtifacts.get(filename);
525
+ finalArtifacts.set(filename, { title: op.title || existing?.title || filename, content: op.content });
526
+ }
527
+ break;
528
+ }
529
+ case "update": {
530
+ const existing = finalArtifacts.get(filename);
531
+ if (!existing) break; // skip invalid update (shouldn't happen for successful results)
532
+ if (op.old_str !== undefined && op.new_str !== undefined) {
533
+ existing.content = existing.content.replace(op.old_str, op.new_str);
534
+ finalArtifacts.set(filename, existing);
535
+ }
536
+ break;
537
+ }
538
+ case "delete": {
539
+ finalArtifacts.delete(filename);
540
+ break;
541
+ }
542
+ case "get":
543
+ case "logs":
544
+ // Ignored above, just for completeness
545
+ break;
546
+ }
547
+ }
548
+
549
+ // 4) Reset current UI state before bulk create
550
+ this._artifacts.clear();
551
+ this.artifactElements.forEach((el) => {
552
+ el.remove();
553
+ });
554
+ this.artifactElements.clear();
555
+ this._activeFilename = null;
556
+ this._artifacts = new Map(this._artifacts);
557
+
558
+ // 5) Create artifacts in a single pass without waiting for iframe execution or tab switching
559
+ for (const [filename, { title, content }] of finalArtifacts.entries()) {
560
+ const createParams: ArtifactsParams = { command: "create", filename, title, content } as const;
561
+ try {
562
+ await this.createArtifact(createParams, { skipWait: true, silent: true });
563
+ } catch {
564
+ // Ignore failures during reconstruction
565
+ }
566
+ }
567
+
568
+ // 6) Show first artifact if any exist, and notify listeners once
569
+ if (!this._activeFilename && this._artifacts.size > 0) {
570
+ this.showArtifact(Array.from(this._artifacts.keys())[0]);
571
+ }
572
+ this.onArtifactsChange?.();
573
+ this.requestUpdate();
574
+ }
575
+
576
+ // Core command executor
577
+ private async executeCommand(
578
+ params: ArtifactsParams,
579
+ options: { skipWait?: boolean; silent?: boolean } = {},
580
+ ): Promise<string> {
581
+ switch (params.command) {
582
+ case "create":
583
+ return await this.createArtifact(params, options);
584
+ case "update":
585
+ return await this.updateArtifact(params, options);
586
+ case "rewrite":
587
+ return await this.rewriteArtifact(params, options);
588
+ case "get":
589
+ return this.getArtifact(params);
590
+ case "delete":
591
+ return this.deleteArtifact(params);
592
+ case "logs":
593
+ return this.getLogs(params);
594
+ default:
595
+ // Should never happen with TypeBox validation
596
+ return `Error: Unknown command ${(params as any).command}`;
597
+ }
598
+ }
599
+
600
+ // Wait for HTML artifact execution and get logs
601
+ private async waitForHtmlExecution(filename: string): Promise<string> {
602
+ const element = this.artifactElements.get(filename);
603
+ if (!(element instanceof HtmlArtifact)) {
604
+ return "";
605
+ }
606
+
607
+ return new Promise((resolve) => {
608
+ let resolved = false;
609
+
610
+ // Listen for the execution-complete message
611
+ const messageHandler = (event: MessageEvent) => {
612
+ if (event.data?.type === "execution-complete" && event.data?.artifactId === filename) {
613
+ if (!resolved) {
614
+ resolved = true;
615
+ window.removeEventListener("message", messageHandler);
616
+
617
+ // Get the logs from the element
618
+ const logs = element.getLogs();
619
+ if (logs.includes("[error]")) {
620
+ resolve(`\n\nExecution completed with errors:\n${logs}`);
621
+ } else if (logs !== `No logs for ${filename}`) {
622
+ resolve(`\n\nExecution logs:\n${logs}`);
623
+ } else {
624
+ resolve("");
625
+ }
626
+ }
627
+ }
628
+ };
629
+
630
+ window.addEventListener("message", messageHandler);
631
+
632
+ // Fallback timeout in case the message never arrives
633
+ setTimeout(() => {
634
+ if (!resolved) {
635
+ resolved = true;
636
+ window.removeEventListener("message", messageHandler);
637
+
638
+ // Get whatever logs we have so far
639
+ const logs = element.getLogs();
640
+ if (logs.includes("[error]")) {
641
+ resolve(`\n\nExecution timed out with errors:\n${logs}`);
642
+ } else if (logs !== `No logs for ${filename}`) {
643
+ resolve(`\n\nExecution timed out. Partial logs:\n${logs}`);
644
+ } else {
645
+ resolve("");
646
+ }
647
+ }
648
+ }, 1500);
649
+ });
650
+ }
651
+
652
+ private async createArtifact(
653
+ params: ArtifactsParams,
654
+ options: { skipWait?: boolean; silent?: boolean } = {},
655
+ ): Promise<string> {
656
+ if (!params.filename || !params.content) {
657
+ return "Error: create command requires filename and content";
658
+ }
659
+ if (this._artifacts.has(params.filename)) {
660
+ return `Error: File ${params.filename} already exists`;
661
+ }
662
+
663
+ const title = params.title || params.filename;
664
+ const artifact: Artifact = {
665
+ filename: params.filename,
666
+ title: title,
667
+ content: params.content,
668
+ createdAt: new Date(),
669
+ updatedAt: new Date(),
670
+ };
671
+ this._artifacts.set(params.filename, artifact);
672
+ this._artifacts = new Map(this._artifacts);
673
+
674
+ // Create or update element
675
+ this.getOrCreateArtifactElement(params.filename, params.content, title);
676
+ if (!options.silent) {
677
+ this.showArtifact(params.filename);
678
+ this.onArtifactsChange?.();
679
+ this.requestUpdate();
680
+ }
681
+
682
+ // For HTML files, wait for execution
683
+ let result = `Created file ${params.filename}`;
684
+ if (this.getFileType(params.filename) === "html" && !options.skipWait) {
685
+ const logs = await this.waitForHtmlExecution(params.filename);
686
+ result += logs;
687
+ }
688
+
689
+ return result;
690
+ }
691
+
692
+ private async updateArtifact(
693
+ params: ArtifactsParams,
694
+ options: { skipWait?: boolean; silent?: boolean } = {},
695
+ ): Promise<string> {
696
+ const artifact = this._artifacts.get(params.filename);
697
+ if (!artifact) {
698
+ const files = Array.from(this._artifacts.keys());
699
+ if (files.length === 0) return `Error: File ${params.filename} not found. No files have been created yet.`;
700
+ return `Error: File ${params.filename} not found. Available files: ${files.join(", ")}`;
701
+ }
702
+ if (!params.old_str || params.new_str === undefined) {
703
+ return "Error: update command requires old_str and new_str";
704
+ }
705
+ if (!artifact.content.includes(params.old_str)) {
706
+ return `Error: String not found in file. Here is the full content:\n\n${artifact.content}`;
707
+ }
708
+
709
+ artifact.content = artifact.content.replace(params.old_str, params.new_str);
710
+ artifact.updatedAt = new Date();
711
+ this._artifacts.set(params.filename, artifact);
712
+
713
+ // Update element
714
+ this.getOrCreateArtifactElement(params.filename, artifact.content, artifact.title);
715
+ if (!options.silent) {
716
+ this.onArtifactsChange?.();
717
+ this.requestUpdate();
718
+ }
719
+
720
+ // Show the artifact
721
+ this.showArtifact(params.filename);
722
+
723
+ // For HTML files, wait for execution
724
+ let result = `Updated file ${params.filename}`;
725
+ if (this.getFileType(params.filename) === "html" && !options.skipWait) {
726
+ const logs = await this.waitForHtmlExecution(params.filename);
727
+ result += logs;
728
+ }
729
+
730
+ return result;
731
+ }
732
+
733
+ private async rewriteArtifact(
734
+ params: ArtifactsParams,
735
+ options: { skipWait?: boolean; silent?: boolean } = {},
736
+ ): Promise<string> {
737
+ const artifact = this._artifacts.get(params.filename);
738
+ if (!artifact) {
739
+ const files = Array.from(this._artifacts.keys());
740
+ if (files.length === 0) return `Error: File ${params.filename} not found. No files have been created yet.`;
741
+ return `Error: File ${params.filename} not found. Available files: ${files.join(", ")}`;
742
+ }
743
+ if (!params.content) {
744
+ return "Error: rewrite command requires content";
745
+ }
746
+
747
+ artifact.content = params.content;
748
+ if (params.title) artifact.title = params.title;
749
+ artifact.updatedAt = new Date();
750
+ this._artifacts.set(params.filename, artifact);
751
+
752
+ // Update element
753
+ this.getOrCreateArtifactElement(params.filename, artifact.content, artifact.title);
754
+ if (!options.silent) {
755
+ this.onArtifactsChange?.();
756
+ }
757
+
758
+ // Show the artifact
759
+ this.showArtifact(params.filename);
760
+
761
+ // For HTML files, wait for execution
762
+ let result = `Rewrote file ${params.filename}`;
763
+ if (this.getFileType(params.filename) === "html" && !options.skipWait) {
764
+ const logs = await this.waitForHtmlExecution(params.filename);
765
+ result += logs;
766
+ }
767
+
768
+ return result;
769
+ }
770
+
771
+ private getArtifact(params: ArtifactsParams): string {
772
+ const artifact = this._artifacts.get(params.filename);
773
+ if (!artifact) {
774
+ const files = Array.from(this._artifacts.keys());
775
+ if (files.length === 0) return `Error: File ${params.filename} not found. No files have been created yet.`;
776
+ return `Error: File ${params.filename} not found. Available files: ${files.join(", ")}`;
777
+ }
778
+ return artifact.content;
779
+ }
780
+
781
+ private deleteArtifact(params: ArtifactsParams): string {
782
+ const artifact = this._artifacts.get(params.filename);
783
+ if (!artifact) {
784
+ const files = Array.from(this._artifacts.keys());
785
+ if (files.length === 0) return `Error: File ${params.filename} not found. No files have been created yet.`;
786
+ return `Error: File ${params.filename} not found. Available files: ${files.join(", ")}`;
787
+ }
788
+
789
+ this._artifacts.delete(params.filename);
790
+ this._artifacts = new Map(this._artifacts);
791
+
792
+ // Remove element
793
+ const element = this.artifactElements.get(params.filename);
794
+ if (element) {
795
+ element.remove();
796
+ this.artifactElements.delete(params.filename);
797
+ }
798
+
799
+ // Show another artifact if this was active
800
+ if (this._activeFilename === params.filename) {
801
+ const remaining = Array.from(this._artifacts.keys());
802
+ if (remaining.length > 0) {
803
+ this.showArtifact(remaining[0]);
804
+ } else {
805
+ this._activeFilename = null;
806
+ this.requestUpdate();
807
+ }
808
+ }
809
+ this.onArtifactsChange?.();
810
+ this.requestUpdate();
811
+
812
+ return `Deleted file ${params.filename}`;
813
+ }
814
+
815
+ private getLogs(params: ArtifactsParams): string {
816
+ const element = this.artifactElements.get(params.filename);
817
+ if (!element) {
818
+ const files = Array.from(this._artifacts.keys());
819
+ if (files.length === 0) return `Error: File ${params.filename} not found. No files have been created yet.`;
820
+ return `Error: File ${params.filename} not found. Available files: ${files.join(", ")}`;
821
+ }
822
+
823
+ if (!(element instanceof HtmlArtifact)) {
824
+ return `Error: File ${params.filename} is not an HTML file. Logs are only available for HTML files.`;
825
+ }
826
+
827
+ return element.getLogs();
828
+ }
829
+
830
+ override render(): TemplateResult {
831
+ const artifacts = Array.from(this._artifacts.values());
832
+
833
+ // Panel is hidden when collapsed OR when there are no artifacts
834
+ const showPanel = artifacts.length > 0 && !this.collapsed;
835
+
836
+ return html`
837
+ <div
838
+ class="${showPanel ? "" : "hidden"} ${
839
+ this.overlay ? "fixed inset-0 z-40 pointer-events-auto backdrop-blur-sm bg-background/95" : "relative"
840
+ } h-full flex flex-col bg-background text-card-foreground ${
841
+ !this.overlay ? "border-l border-border" : ""
842
+ } overflow-hidden shadow-xl"
843
+ >
844
+ <!-- Tab bar (always shown when there are artifacts) -->
845
+ <div class="flex items-center justify-between border-b border-border bg-background">
846
+ <div class="flex overflow-x-auto">
847
+ ${artifacts.map((a) => {
848
+ const isActive = a.filename === this._activeFilename;
849
+ const activeClass = isActive
850
+ ? "border-primary text-primary"
851
+ : "border-transparent text-muted-foreground hover:text-foreground";
852
+ return html`
853
+ <button
854
+ class="px-3 py-2 whitespace-nowrap border-b-2 ${activeClass}"
855
+ @click=${() => this.showArtifact(a.filename)}
856
+ >
857
+ <span class="font-mono text-xs">${a.filename}</span>
858
+ </button>
859
+ `;
860
+ })}
861
+ </div>
862
+ <div class="flex items-center gap-1 px-2">
863
+ ${(() => {
864
+ const active = this._activeFilename ? this.artifactElements.get(this._activeFilename) : undefined;
865
+ return active ? active.getHeaderButtons() : "";
866
+ })()}
867
+ ${Button({
868
+ variant: "ghost",
869
+ size: "sm",
870
+ onClick: () => this.onClose?.(),
871
+ title: i18n("Close artifacts"),
872
+ children: icon(X, "sm"),
873
+ })}
874
+ </div>
875
+ </div>
876
+
877
+ <!-- Content area where artifact elements are added programmatically -->
878
+ <div class="flex-1 overflow-hidden" ${ref(this.contentRef)}></div>
879
+ </div>
880
+ `;
881
+ }
882
+ }
883
+
884
+ declare global {
885
+ interface HTMLElementTagNameMap {
886
+ "artifacts-panel": ArtifactsPanel;
887
+ }
888
+ }