@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,272 @@
1
+ import { Button, html, icon } from "@mariozechner/mini-lit";
2
+ import type { Model } from "@mariozechner/pi-ai";
3
+ import { LitElement } from "lit";
4
+ import { customElement, property, state } from "lit/decorators.js";
5
+ import { createRef, ref } from "lit/directives/ref.js";
6
+ import { Loader2, Paperclip, Send, Sparkles, Square } from "lucide";
7
+ import "./AttachmentTile.js";
8
+ import { type Attachment, loadAttachment } from "../utils/attachment-utils.js";
9
+ import { i18n } from "../utils/i18n.js";
10
+
11
+ @customElement("message-editor")
12
+ export class MessageEditor extends LitElement {
13
+ private _value = "";
14
+ private textareaRef = createRef<HTMLTextAreaElement>();
15
+
16
+ @property()
17
+ get value() {
18
+ return this._value;
19
+ }
20
+
21
+ set value(val: string) {
22
+ const oldValue = this._value;
23
+ this._value = val;
24
+ this.requestUpdate("value", oldValue);
25
+ this.updateComplete.then(() => {
26
+ const textarea = this.textareaRef.value;
27
+ if (textarea) {
28
+ textarea.style.height = "auto";
29
+ textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`;
30
+ }
31
+ });
32
+ }
33
+
34
+ @property() isStreaming = false;
35
+ @property() currentModel?: Model<any>;
36
+ @property() showAttachmentButton = true;
37
+ @property() showModelSelector = true;
38
+ @property() showThinking = false; // Disabled for now
39
+ @property() onInput?: (value: string) => void;
40
+ @property() onSend?: (input: string, attachments: Attachment[]) => void;
41
+ @property() onAbort?: () => void;
42
+ @property() onModelSelect?: () => void;
43
+ @property() onFilesChange?: (files: Attachment[]) => void;
44
+ @property() attachments: Attachment[] = [];
45
+ @property() maxFiles = 10;
46
+ @property() maxFileSize = 20 * 1024 * 1024; // 20MB
47
+ @property() acceptedTypes =
48
+ "image/*,application/pdf,.docx,.pptx,.xlsx,.xls,.txt,.md,.json,.xml,.html,.css,.js,.ts,.jsx,.tsx,.yml,.yaml";
49
+
50
+ @state() processingFiles = false;
51
+ private fileInputRef = createRef<HTMLInputElement>();
52
+
53
+ protected override createRenderRoot(): HTMLElement | DocumentFragment {
54
+ return this;
55
+ }
56
+
57
+ private handleTextareaInput = (e: Event) => {
58
+ const textarea = e.target as HTMLTextAreaElement;
59
+ this.value = textarea.value;
60
+ textarea.style.height = "auto";
61
+ textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`;
62
+ this.onInput?.(this.value);
63
+ };
64
+
65
+ private handleKeyDown = (e: KeyboardEvent) => {
66
+ if (e.key === "Enter" && !e.shiftKey) {
67
+ e.preventDefault();
68
+ if (!this.isStreaming && !this.processingFiles && (this.value.trim() || this.attachments.length > 0)) {
69
+ this.handleSend();
70
+ }
71
+ } else if (e.key === "Escape" && this.isStreaming) {
72
+ e.preventDefault();
73
+ this.onAbort?.();
74
+ }
75
+ };
76
+
77
+ private handleSend = () => {
78
+ this.onSend?.(this.value, this.attachments);
79
+ };
80
+
81
+ private handleAttachmentClick = () => {
82
+ this.fileInputRef.value?.click();
83
+ };
84
+
85
+ private async handleFilesSelected(e: Event) {
86
+ const input = e.target as HTMLInputElement;
87
+ const files = Array.from(input.files || []);
88
+ if (files.length === 0) return;
89
+
90
+ if (files.length + this.attachments.length > this.maxFiles) {
91
+ alert(`Maximum ${this.maxFiles} files allowed`);
92
+ input.value = "";
93
+ return;
94
+ }
95
+
96
+ this.processingFiles = true;
97
+ const newAttachments: Attachment[] = [];
98
+
99
+ for (const file of files) {
100
+ try {
101
+ if (file.size > this.maxFileSize) {
102
+ alert(`${file.name} exceeds maximum size of ${Math.round(this.maxFileSize / 1024 / 1024)}MB`);
103
+ continue;
104
+ }
105
+
106
+ const attachment = await loadAttachment(file);
107
+ newAttachments.push(attachment);
108
+ } catch (error) {
109
+ console.error(`Error processing ${file.name}:`, error);
110
+ alert(`Failed to process ${file.name}: ${String(error)}`);
111
+ }
112
+ }
113
+
114
+ this.attachments = [...this.attachments, ...newAttachments];
115
+ this.onFilesChange?.(this.attachments);
116
+ this.processingFiles = false;
117
+ input.value = ""; // Reset input
118
+ }
119
+
120
+ private removeFile(fileId: string) {
121
+ this.attachments = this.attachments.filter((f) => f.id !== fileId);
122
+ this.onFilesChange?.(this.attachments);
123
+ }
124
+
125
+ private adjustTextareaHeight() {
126
+ const textarea = this.textareaRef.value;
127
+ if (textarea) {
128
+ // Reset height to auto to get accurate scrollHeight
129
+ textarea.style.height = "auto";
130
+ // Only adjust if there's content, otherwise keep minimal height
131
+ if (this.value.trim()) {
132
+ textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`;
133
+ }
134
+ }
135
+ }
136
+
137
+ override firstUpdated() {
138
+ const textarea = this.textareaRef.value;
139
+ if (textarea) {
140
+ // Don't adjust height on first render - let it be minimal
141
+ textarea.focus();
142
+ }
143
+ }
144
+
145
+ override updated() {
146
+ // Only adjust height when component updates if there's content
147
+ if (this.value) {
148
+ this.adjustTextareaHeight();
149
+ }
150
+ }
151
+
152
+ override render() {
153
+ return html`
154
+ <div class="bg-card rounded-xl border border-border shadow-sm">
155
+ <!-- Attachments -->
156
+ ${
157
+ this.attachments.length > 0
158
+ ? html`
159
+ <div class="px-4 pt-3 pb-2 flex flex-wrap gap-2">
160
+ ${this.attachments.map(
161
+ (attachment) => html`
162
+ <attachment-tile
163
+ .attachment=${attachment}
164
+ .showDelete=${true}
165
+ .onDelete=${() => this.removeFile(attachment.id)}
166
+ ></attachment-tile>
167
+ `,
168
+ )}
169
+ </div>
170
+ `
171
+ : ""
172
+ }
173
+
174
+ <textarea
175
+ class="w-full bg-transparent p-4 text-foreground placeholder-muted-foreground outline-none resize-none overflow-y-auto"
176
+ placeholder=${i18n("Type a message...")}
177
+ rows="1"
178
+ style="max-height: 200px;"
179
+ .value=${this.value}
180
+ @input=${this.handleTextareaInput}
181
+ @keydown=${this.handleKeyDown}
182
+ ${ref(this.textareaRef)}
183
+ ></textarea>
184
+
185
+ <!-- Hidden file input -->
186
+ <input
187
+ type="file"
188
+ ${ref(this.fileInputRef)}
189
+ @change=${this.handleFilesSelected}
190
+ accept=${this.acceptedTypes}
191
+ multiple
192
+ style="display: none;"
193
+ />
194
+
195
+ <!-- Button Row -->
196
+ <div class="px-2 pb-2 flex items-center justify-between">
197
+ <!-- Left side - attachment and quick action buttons -->
198
+ <div class="flex gap-2 items-center">
199
+ ${
200
+ this.showAttachmentButton
201
+ ? this.processingFiles
202
+ ? html`
203
+ <div class="h-8 w-8 flex items-center justify-center">
204
+ ${icon(Loader2, "sm", "animate-spin text-muted-foreground")}
205
+ </div>
206
+ `
207
+ : html`
208
+ ${Button({
209
+ variant: "ghost",
210
+ size: "icon",
211
+ className: "h-8 w-8",
212
+ onClick: this.handleAttachmentClick,
213
+ children: icon(Paperclip, "sm"),
214
+ })}
215
+ `
216
+ : ""
217
+ }
218
+ </div>
219
+
220
+ <!-- Model selector and send on the right -->
221
+ <div class="flex gap-2 items-center">
222
+ ${
223
+ this.showModelSelector && this.currentModel
224
+ ? html`
225
+ ${Button({
226
+ variant: "ghost",
227
+ size: "sm",
228
+ onClick: () => {
229
+ // Focus textarea before opening model selector so focus returns there
230
+ this.textareaRef.value?.focus();
231
+ // Wait for next frame to ensure focus takes effect before dialog captures it
232
+ requestAnimationFrame(() => {
233
+ this.onModelSelect?.();
234
+ });
235
+ },
236
+ children: html`
237
+ ${icon(Sparkles, "sm")}
238
+ <span class="ml-1">${this.currentModel.id}</span>
239
+ `,
240
+ className: "h-8 text-xs truncate",
241
+ })}
242
+ `
243
+ : ""
244
+ }
245
+ ${
246
+ this.isStreaming
247
+ ? html`
248
+ ${Button({
249
+ variant: "ghost",
250
+ size: "icon",
251
+ onClick: this.onAbort,
252
+ children: icon(Square, "sm"),
253
+ className: "h-8 w-8",
254
+ })}
255
+ `
256
+ : html`
257
+ ${Button({
258
+ variant: "ghost",
259
+ size: "icon",
260
+ onClick: this.handleSend,
261
+ disabled: (!this.value.trim() && this.attachments.length === 0) || this.processingFiles,
262
+ children: html`<div style="transform: rotate(-45deg)">${icon(Send, "sm")}</div>`,
263
+ className: "h-8 w-8",
264
+ })}
265
+ `
266
+ }
267
+ </div>
268
+ </div>
269
+ </div>
270
+ `;
271
+ }
272
+ }
@@ -0,0 +1,82 @@
1
+ import { html } from "@mariozechner/mini-lit";
2
+ import type {
3
+ AgentTool,
4
+ AssistantMessage as AssistantMessageType,
5
+ Message,
6
+ ToolResultMessage as ToolResultMessageType,
7
+ } from "@mariozechner/pi-ai";
8
+ import { LitElement, type TemplateResult } from "lit";
9
+ import { property } from "lit/decorators.js";
10
+ import { repeat } from "lit/directives/repeat.js";
11
+
12
+ export class MessageList extends LitElement {
13
+ @property({ type: Array }) messages: Message[] = [];
14
+ @property({ type: Array }) tools: AgentTool[] = [];
15
+ @property({ type: Object }) pendingToolCalls?: Set<string>;
16
+ @property({ type: Boolean }) isStreaming: boolean = false;
17
+
18
+ protected override createRenderRoot(): HTMLElement | DocumentFragment {
19
+ return this;
20
+ }
21
+
22
+ override connectedCallback(): void {
23
+ super.connectedCallback();
24
+ this.style.display = "block";
25
+ }
26
+
27
+ private buildRenderItems() {
28
+ // Map tool results by call id for quick lookup
29
+ const resultByCallId = new Map<string, ToolResultMessageType>();
30
+ for (const message of this.messages) {
31
+ if (message.role === "toolResult") {
32
+ resultByCallId.set(message.toolCallId, message);
33
+ }
34
+ }
35
+
36
+ const items: Array<{ key: string; template: TemplateResult }> = [];
37
+ let index = 0;
38
+ for (const msg of this.messages) {
39
+ if (msg.role === "user") {
40
+ items.push({
41
+ key: `msg:${index}`,
42
+ template: html`<user-message .message=${msg}></user-message>`,
43
+ });
44
+ index++;
45
+ } else if (msg.role === "assistant") {
46
+ const amsg = msg as AssistantMessageType;
47
+ items.push({
48
+ key: `msg:${index}`,
49
+ template: html`<assistant-message
50
+ .message=${amsg}
51
+ .tools=${this.tools}
52
+ .isStreaming=${this.isStreaming}
53
+ .pendingToolCalls=${this.pendingToolCalls}
54
+ .toolResultsById=${resultByCallId}
55
+ .hideToolCalls=${false}
56
+ ></assistant-message>`,
57
+ });
58
+ index++;
59
+ } else {
60
+ // Skip standalone toolResult messages; they are rendered via paired tool-message above
61
+ // For completeness, other roles are not expected
62
+ }
63
+ }
64
+ return items;
65
+ }
66
+
67
+ override render() {
68
+ const items = this.buildRenderItems();
69
+ return html`<div class="flex flex-col gap-3">
70
+ ${repeat(
71
+ items,
72
+ (it) => it.key,
73
+ (it) => it.template,
74
+ )}
75
+ </div>`;
76
+ }
77
+ }
78
+
79
+ // Register custom element
80
+ if (!customElements.get("message-list")) {
81
+ customElements.define("message-list", MessageList);
82
+ }
@@ -0,0 +1,310 @@
1
+ import { Button, html, icon } from "@mariozechner/mini-lit";
2
+ import type {
3
+ AgentTool,
4
+ AssistantMessage as AssistantMessageType,
5
+ ToolCall,
6
+ ToolResultMessage as ToolResultMessageType,
7
+ UserMessage as UserMessageType,
8
+ } from "@mariozechner/pi-ai";
9
+ import type { AgentToolResult } from "@mariozechner/pi-ai/dist/agent/types.js";
10
+ import { LitElement, type TemplateResult } from "lit";
11
+ import { customElement, property, state } from "lit/decorators.js";
12
+ import { Bug, Loader, Wrench } from "lucide";
13
+ import { renderToolParams, renderToolResult } from "../tools/index.js";
14
+ import type { Attachment } from "../utils/attachment-utils.js";
15
+ import { formatUsage } from "../utils/format.js";
16
+ import { i18n } from "../utils/i18n.js";
17
+
18
+ export type UserMessageWithAttachments = UserMessageType & { attachments?: Attachment[] };
19
+ export type AppMessage = AssistantMessageType | UserMessageWithAttachments | ToolResultMessageType;
20
+
21
+ @customElement("user-message")
22
+ export class UserMessage extends LitElement {
23
+ @property({ type: Object }) message!: UserMessageWithAttachments;
24
+
25
+ protected override createRenderRoot(): HTMLElement | DocumentFragment {
26
+ return this;
27
+ }
28
+
29
+ override connectedCallback(): void {
30
+ super.connectedCallback();
31
+ this.style.display = "block";
32
+ }
33
+
34
+ override render() {
35
+ const content =
36
+ typeof this.message.content === "string"
37
+ ? this.message.content
38
+ : this.message.content.find((c) => c.type === "text")?.text || "";
39
+
40
+ return html`
41
+ <div class="py-2 px-4 border-l-4 border-accent-foreground/60 text-primary-foreground">
42
+ <markdown-block .content=${content}></markdown-block>
43
+ ${
44
+ this.message.attachments && this.message.attachments.length > 0
45
+ ? html`
46
+ <div class="mt-3 flex flex-wrap gap-2">
47
+ ${this.message.attachments.map(
48
+ (attachment) => html` <attachment-tile .attachment=${attachment}></attachment-tile> `,
49
+ )}
50
+ </div>
51
+ `
52
+ : ""
53
+ }
54
+ </div>
55
+ `;
56
+ }
57
+ }
58
+
59
+ @customElement("assistant-message")
60
+ export class AssistantMessage extends LitElement {
61
+ @property({ type: Object }) message!: AssistantMessageType;
62
+ @property({ type: Array }) tools?: AgentTool<any>[];
63
+ @property({ type: Object }) pendingToolCalls?: Set<string>;
64
+ @property({ type: Boolean }) hideToolCalls = false;
65
+ @property({ type: Object }) toolResultsById?: Map<string, ToolResultMessageType>;
66
+ @property({ type: Boolean }) isStreaming: boolean = false;
67
+
68
+ protected override createRenderRoot(): HTMLElement | DocumentFragment {
69
+ return this;
70
+ }
71
+
72
+ override connectedCallback(): void {
73
+ super.connectedCallback();
74
+ this.style.display = "block";
75
+ }
76
+
77
+ override render() {
78
+ // Render content in the order it appears
79
+ const orderedParts: TemplateResult[] = [];
80
+
81
+ for (const chunk of this.message.content) {
82
+ if (chunk.type === "text" && chunk.text.trim() !== "") {
83
+ orderedParts.push(html`<markdown-block .content=${chunk.text}></markdown-block>`);
84
+ } else if (chunk.type === "thinking" && chunk.thinking.trim() !== "") {
85
+ orderedParts.push(html` <markdown-block .content=${chunk.thinking} .isThinking=${true}></markdown-block> `);
86
+ } else if (chunk.type === "toolCall") {
87
+ if (!this.hideToolCalls) {
88
+ const tool = this.tools?.find((t) => t.name === chunk.name);
89
+ const pending = this.pendingToolCalls?.has(chunk.id) ?? false;
90
+ const result = this.toolResultsById?.get(chunk.id);
91
+ const aborted = !pending && !result && !this.isStreaming;
92
+ orderedParts.push(
93
+ html`<tool-message
94
+ .tool=${tool}
95
+ .toolCall=${chunk}
96
+ .result=${result}
97
+ .pending=${pending}
98
+ .aborted=${aborted}
99
+ .isStreaming=${this.isStreaming}
100
+ ></tool-message>`,
101
+ );
102
+ }
103
+ }
104
+ }
105
+
106
+ return html`
107
+ <div>
108
+ ${orderedParts.length ? html` <div class="px-4 flex flex-col gap-3">${orderedParts}</div> ` : ""}
109
+ ${
110
+ this.message.usage
111
+ ? html` <div class="px-4 mt-2 text-xs text-muted-foreground">${formatUsage(this.message.usage)}</div> `
112
+ : ""
113
+ }
114
+ ${
115
+ this.message.stopReason === "error" && this.message.errorMessage
116
+ ? html`
117
+ <div class="mx-4 mt-3 p-3 bg-destructive/10 text-destructive rounded-lg text-sm overflow-hidden">
118
+ <strong>${i18n("Error:")}</strong> ${this.message.errorMessage}
119
+ </div>
120
+ `
121
+ : ""
122
+ }
123
+ ${
124
+ this.message.stopReason === "aborted"
125
+ ? html`<span class="text-sm text-destructive italic">${i18n("Request aborted")}</span>`
126
+ : ""
127
+ }
128
+ </div>
129
+ `;
130
+ }
131
+ }
132
+
133
+ @customElement("tool-message-debug")
134
+ export class ToolMessageDebugView extends LitElement {
135
+ @property({ type: Object }) callArgs: any;
136
+ @property({ type: String }) result?: AgentToolResult<any>;
137
+ @property({ type: Boolean }) hasResult: boolean = false;
138
+
139
+ protected override createRenderRoot(): HTMLElement | DocumentFragment {
140
+ return this; // light DOM for shared styles
141
+ }
142
+
143
+ override connectedCallback(): void {
144
+ super.connectedCallback();
145
+ this.style.display = "block";
146
+ }
147
+
148
+ private pretty(value: unknown): { content: string; isJson: boolean } {
149
+ try {
150
+ if (typeof value === "string") {
151
+ const maybeJson = JSON.parse(value);
152
+ return { content: JSON.stringify(maybeJson, null, 2), isJson: true };
153
+ }
154
+ return { content: JSON.stringify(value, null, 2), isJson: true };
155
+ } catch {
156
+ return { content: typeof value === "string" ? value : String(value), isJson: false };
157
+ }
158
+ }
159
+
160
+ override render() {
161
+ const output = this.pretty(this.result?.output);
162
+ const details = this.pretty(this.result?.details);
163
+
164
+ return html`
165
+ <div class="mt-3 flex flex-col gap-2">
166
+ <div>
167
+ <div class="text-xs font-medium mb-1 text-muted-foreground">${i18n("Call")}</div>
168
+ <code-block .code=${this.pretty(this.callArgs).content} language="json"></code-block>
169
+ </div>
170
+ <div>
171
+ <div class="text-xs font-medium mb-1 text-muted-foreground">${i18n("Result")}</div>
172
+ ${
173
+ this.hasResult
174
+ ? html`<code-block .code=${output.content} language="${output.isJson ? "json" : "text"}"></code-block>
175
+ <code-block .code=${details.content} language="${details.isJson ? "json" : "text"}"></code-block>`
176
+ : html`<div class="text-xs text-muted-foreground">${i18n("(no result)")}</div>`
177
+ }
178
+ </div>
179
+ </div>
180
+ `;
181
+ }
182
+ }
183
+
184
+ @customElement("tool-message")
185
+ export class ToolMessage extends LitElement {
186
+ @property({ type: Object }) toolCall!: ToolCall;
187
+ @property({ type: Object }) tool?: AgentTool<any>;
188
+ @property({ type: Object }) result?: ToolResultMessageType;
189
+ @property({ type: Boolean }) pending: boolean = false;
190
+ @property({ type: Boolean }) aborted: boolean = false;
191
+ @property({ type: Boolean }) isStreaming: boolean = false;
192
+ @state() private _showDebug = false;
193
+
194
+ protected override createRenderRoot(): HTMLElement | DocumentFragment {
195
+ return this;
196
+ }
197
+
198
+ override connectedCallback(): void {
199
+ super.connectedCallback();
200
+ this.style.display = "block";
201
+ }
202
+
203
+ private toggleDebug = () => {
204
+ this._showDebug = !this._showDebug;
205
+ };
206
+
207
+ override render() {
208
+ const toolLabel = this.tool?.label || this.toolCall.name;
209
+ const toolName = this.tool?.name || this.toolCall.name;
210
+ const isError = this.result?.isError === true;
211
+ const hasResult = !!this.result;
212
+
213
+ let statusIcon: TemplateResult;
214
+ if (this.pending || (this.isStreaming && !hasResult)) {
215
+ statusIcon = html`<span class="inline-block text-muted-foreground animate-spin">${icon(Loader, "sm")}</span>`;
216
+ } else if (this.aborted && !hasResult) {
217
+ statusIcon = html`<span class="inline-block text-destructive">${icon(Wrench, "sm")}</span>`;
218
+ } else if (hasResult && isError) {
219
+ statusIcon = html`<span class="inline-block text-destructive">${icon(Wrench, "sm")}</span>`;
220
+ } else if (hasResult) {
221
+ statusIcon = html`<span class="inline-block text-muted-foreground">${icon(Wrench, "sm")}</span>`;
222
+ } else {
223
+ statusIcon = html`<span class="inline-block text-muted-foreground">${icon(Wrench, "sm")}</span>`;
224
+ }
225
+
226
+ // Normalize error text
227
+ let errorMessage = this.result?.output || "";
228
+ if (isError) {
229
+ try {
230
+ const parsed = JSON.parse(errorMessage);
231
+ if ((parsed as any).error) errorMessage = (parsed as any).error;
232
+ else if ((parsed as any).message) errorMessage = (parsed as any).message;
233
+ } catch {}
234
+ errorMessage = errorMessage.replace(/^(Tool )?Error:\s*/i, "");
235
+ errorMessage = errorMessage.replace(/^Error:\s*/i, "");
236
+ }
237
+
238
+ const paramsTpl = renderToolParams(
239
+ toolName,
240
+ this.toolCall.arguments,
241
+ this.isStreaming || (this.pending && !hasResult),
242
+ );
243
+ const resultTpl =
244
+ hasResult && !isError ? renderToolResult(toolName, this.toolCall.arguments, this.result!) : undefined;
245
+
246
+ return html`
247
+ <div class="p-2.5 border border-border rounded-md bg-card text-card-foreground">
248
+ <div class="flex items-center justify-between text-xs text-muted-foreground">
249
+ <div class="flex items-center gap-2">
250
+ ${statusIcon}
251
+ <span class="font-medium">${toolLabel}</span>
252
+ </div>
253
+ ${Button({
254
+ variant: this._showDebug ? "default" : "ghost",
255
+ size: "sm",
256
+ onClick: this.toggleDebug,
257
+ children: icon(Bug, "sm"),
258
+ className: "text-muted-foreground",
259
+ })}
260
+ </div>
261
+
262
+ ${
263
+ this._showDebug
264
+ ? html`<tool-message-debug
265
+ .callArgs=${this.toolCall.arguments}
266
+ .result=${this.result}
267
+ .hasResult=${!!this.result}
268
+ ></tool-message-debug>`
269
+ : html`
270
+ <div class="mt-2 text-sm text-muted-foreground">${paramsTpl}</div>
271
+ ${
272
+ this.pending && !hasResult
273
+ ? html`<div class="mt-2 text-sm text-muted-foreground">${i18n("Waiting for tool result…")}</div>`
274
+ : ""
275
+ }
276
+ ${
277
+ this.aborted && !hasResult
278
+ ? html`<div class="mt-2 text-sm text-muted-foreground">${i18n("Call was aborted; no result.")}</div>`
279
+ : ""
280
+ }
281
+ ${
282
+ hasResult && isError
283
+ ? html`<div class="mt-2 p-2 border border-destructive rounded bg-destructive/10 text-sm text-destructive">
284
+ ${errorMessage}
285
+ </div>`
286
+ : ""
287
+ }
288
+ ${resultTpl ? html`<div class="mt-2">${resultTpl}</div>` : ""}
289
+ `
290
+ }
291
+ </div>
292
+ `;
293
+ }
294
+ }
295
+
296
+ @customElement("aborted-message")
297
+ export class AbortedMessage extends LitElement {
298
+ protected override createRenderRoot(): HTMLElement | DocumentFragment {
299
+ return this;
300
+ }
301
+
302
+ override connectedCallback(): void {
303
+ super.connectedCallback();
304
+ this.style.display = "block";
305
+ }
306
+
307
+ protected override render(): unknown {
308
+ return html`<span class="text-sm text-destructive italic">${i18n("Request aborted")}</span>`;
309
+ }
310
+ }