@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,316 @@
1
+ import { html } from "@mariozechner/mini-lit";
2
+ import type { ToolResultMessage, Usage } from "@mariozechner/pi-ai";
3
+ import { LitElement } from "lit";
4
+ import { customElement, property, query } from "lit/decorators.js";
5
+ import { ModelSelector } from "../dialogs/ModelSelector.js";
6
+ import type { MessageEditor } from "./MessageEditor.js";
7
+ import "./MessageEditor.js";
8
+ import "./MessageList.js";
9
+ import "./Messages.js"; // Import for side effects to register the custom elements
10
+ import type { AgentSession, AgentSessionEvent } from "../state/agent-session.js";
11
+ import { getAppStorage } from "../storage/app-storage.js";
12
+ import "./StreamingMessageContainer.js";
13
+ import type { Attachment } from "../utils/attachment-utils.js";
14
+ import { formatUsage } from "../utils/format.js";
15
+ import { i18n } from "../utils/i18n.js";
16
+ import type { StreamingMessageContainer } from "./StreamingMessageContainer.js";
17
+
18
+ @customElement("agent-interface")
19
+ export class AgentInterface extends LitElement {
20
+ // Optional external session: when provided, this component becomes a view over the session
21
+ @property({ attribute: false }) session?: AgentSession;
22
+ @property() enableAttachments = true;
23
+ @property() enableModelSelector = true;
24
+ @property() enableThinking = true;
25
+ @property() showThemeToggle = false;
26
+ @property() showDebugToggle = false;
27
+ // Optional custom API key prompt handler - if not provided, uses default dialog
28
+ @property({ attribute: false }) onApiKeyRequired?: (provider: string) => Promise<boolean>;
29
+
30
+ // References
31
+ @query("message-editor") private _messageEditor!: MessageEditor;
32
+ @query("streaming-message-container") private _streamingContainer!: StreamingMessageContainer;
33
+
34
+ private _autoScroll = true;
35
+ private _lastScrollTop = 0;
36
+ private _lastClientHeight = 0;
37
+ private _scrollContainer?: HTMLElement;
38
+ private _resizeObserver?: ResizeObserver;
39
+ private _unsubscribeSession?: () => void;
40
+
41
+ public setInput(text: string, attachments?: Attachment[]) {
42
+ const update = () => {
43
+ if (!this._messageEditor) requestAnimationFrame(update);
44
+ else {
45
+ this._messageEditor.value = text;
46
+ this._messageEditor.attachments = attachments || [];
47
+ }
48
+ };
49
+ update();
50
+ }
51
+
52
+ protected override createRenderRoot(): HTMLElement | DocumentFragment {
53
+ return this;
54
+ }
55
+
56
+ override async connectedCallback() {
57
+ super.connectedCallback();
58
+
59
+ this.style.display = "flex";
60
+ this.style.flexDirection = "column";
61
+ this.style.height = "100%";
62
+ this.style.minHeight = "0";
63
+
64
+ // Wait for first render to get scroll container
65
+ await this.updateComplete;
66
+ this._scrollContainer = this.querySelector(".overflow-y-auto") as HTMLElement;
67
+
68
+ if (this._scrollContainer) {
69
+ // Set up ResizeObserver to detect content changes
70
+ this._resizeObserver = new ResizeObserver(() => {
71
+ if (this._autoScroll && this._scrollContainer) {
72
+ this._scrollContainer.scrollTop = this._scrollContainer.scrollHeight;
73
+ }
74
+ });
75
+
76
+ // Observe the content container inside the scroll container
77
+ const contentContainer = this._scrollContainer.querySelector(".max-w-3xl");
78
+ if (contentContainer) {
79
+ this._resizeObserver.observe(contentContainer);
80
+ }
81
+
82
+ // Set up scroll listener with better detection
83
+ this._scrollContainer.addEventListener("scroll", this._handleScroll);
84
+ }
85
+
86
+ // Subscribe to external session if provided
87
+ this.setupSessionSubscription();
88
+
89
+ // Attach debug listener if session provided
90
+ if (this.session) {
91
+ this.session = this.session; // explicitly set to trigger subscription
92
+ }
93
+ }
94
+
95
+ override disconnectedCallback() {
96
+ super.disconnectedCallback();
97
+
98
+ // Clean up observers and listeners
99
+ if (this._resizeObserver) {
100
+ this._resizeObserver.disconnect();
101
+ this._resizeObserver = undefined;
102
+ }
103
+
104
+ if (this._scrollContainer) {
105
+ this._scrollContainer.removeEventListener("scroll", this._handleScroll);
106
+ }
107
+
108
+ if (this._unsubscribeSession) {
109
+ this._unsubscribeSession();
110
+ this._unsubscribeSession = undefined;
111
+ }
112
+ }
113
+
114
+ private setupSessionSubscription() {
115
+ if (this._unsubscribeSession) {
116
+ this._unsubscribeSession();
117
+ this._unsubscribeSession = undefined;
118
+ }
119
+ if (!this.session) return;
120
+ this._unsubscribeSession = this.session.subscribe(async (ev: AgentSessionEvent) => {
121
+ if (ev.type === "state-update") {
122
+ if (this._streamingContainer) {
123
+ this._streamingContainer.isStreaming = ev.state.isStreaming;
124
+ this._streamingContainer.setMessage(ev.state.streamMessage, !ev.state.isStreaming);
125
+ }
126
+ this.requestUpdate();
127
+ } else if (ev.type === "error-no-model") {
128
+ // TODO show some UI feedback
129
+ } else if (ev.type === "error-no-api-key") {
130
+ // Handled by onApiKeyRequired callback
131
+ }
132
+ });
133
+ }
134
+
135
+ private _handleScroll = (_ev: any) => {
136
+ if (!this._scrollContainer) return;
137
+
138
+ const currentScrollTop = this._scrollContainer.scrollTop;
139
+ const scrollHeight = this._scrollContainer.scrollHeight;
140
+ const clientHeight = this._scrollContainer.clientHeight;
141
+ const distanceFromBottom = scrollHeight - currentScrollTop - clientHeight;
142
+
143
+ // Ignore relayout due to message editor getting pushed up by stats
144
+ if (clientHeight < this._lastClientHeight) {
145
+ this._lastClientHeight = clientHeight;
146
+ return;
147
+ }
148
+
149
+ // Only disable auto-scroll if user scrolled UP or is far from bottom
150
+ if (currentScrollTop !== 0 && currentScrollTop < this._lastScrollTop && distanceFromBottom > 50) {
151
+ this._autoScroll = false;
152
+ } else if (distanceFromBottom < 10) {
153
+ // Re-enable if very close to bottom
154
+ this._autoScroll = true;
155
+ }
156
+
157
+ this._lastScrollTop = currentScrollTop;
158
+ this._lastClientHeight = clientHeight;
159
+ };
160
+
161
+ public async sendMessage(input: string, attachments?: Attachment[]) {
162
+ if ((!input.trim() && attachments?.length === 0) || this.session?.state.isStreaming) return;
163
+ const session = this.session;
164
+ if (!session) throw new Error("No session set on AgentInterface");
165
+ if (!session.state.model) throw new Error("No model set on AgentInterface");
166
+
167
+ // Check if API key exists for the provider (only needed in direct mode)
168
+ const provider = session.state.model.provider;
169
+ const apiKey = await getAppStorage().providerKeys.getKey(provider);
170
+
171
+ // If no API key, prompt for it
172
+ if (!apiKey) {
173
+ if (!this.onApiKeyRequired) {
174
+ console.error("No API key configured and no onApiKeyRequired handler set");
175
+ return;
176
+ }
177
+
178
+ const success = await this.onApiKeyRequired(provider);
179
+
180
+ // If still no API key, abort the send
181
+ if (!success) {
182
+ return;
183
+ }
184
+ }
185
+
186
+ // Only clear editor after we know we can send
187
+ this._messageEditor.value = "";
188
+ this._messageEditor.attachments = [];
189
+ this._autoScroll = true; // Enable auto-scroll when sending a message
190
+
191
+ await this.session?.prompt(input, attachments);
192
+ }
193
+
194
+ private renderMessages() {
195
+ if (!this.session)
196
+ return html`<div class="p-4 text-center text-muted-foreground">${i18n("No session available")}</div>`;
197
+ const state = this.session.state;
198
+ // Build a map of tool results to allow inline rendering in assistant messages
199
+ const toolResultsById = new Map<string, ToolResultMessage<any>>();
200
+ for (const message of state.messages) {
201
+ if (message.role === "toolResult") {
202
+ toolResultsById.set(message.toolCallId, message);
203
+ }
204
+ }
205
+ return html`
206
+ <div class="flex flex-col gap-3">
207
+ <!-- Stable messages list - won't re-render during streaming -->
208
+ <message-list
209
+ .messages=${this.session.state.messages}
210
+ .tools=${state.tools}
211
+ .pendingToolCalls=${this.session ? this.session.state.pendingToolCalls : new Set<string>()}
212
+ .isStreaming=${state.isStreaming}
213
+ ></message-list>
214
+
215
+ <!-- Streaming message container - manages its own updates -->
216
+ <streaming-message-container
217
+ class="${state.isStreaming ? "" : "hidden"}"
218
+ .tools=${state.tools}
219
+ .isStreaming=${state.isStreaming}
220
+ .pendingToolCalls=${state.pendingToolCalls}
221
+ .toolResultsById=${toolResultsById}
222
+ ></streaming-message-container>
223
+ </div>
224
+ `;
225
+ }
226
+
227
+ private renderStats() {
228
+ if (!this.session) return html`<div class="text-xs h-5"></div>`;
229
+
230
+ const state = this.session.state;
231
+ const totals = state.messages
232
+ .filter((m) => m.role === "assistant")
233
+ .reduce(
234
+ (acc, msg: any) => {
235
+ const usage = msg.usage;
236
+ if (usage) {
237
+ acc.input += usage.input;
238
+ acc.output += usage.output;
239
+ acc.cacheRead += usage.cacheRead;
240
+ acc.cacheWrite += usage.cacheWrite;
241
+ acc.cost.total += usage.cost.total;
242
+ }
243
+ return acc;
244
+ },
245
+ {
246
+ input: 0,
247
+ output: 0,
248
+ cacheRead: 0,
249
+ cacheWrite: 0,
250
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
251
+ } satisfies Usage,
252
+ );
253
+
254
+ const hasTotals = totals.input || totals.output || totals.cacheRead || totals.cacheWrite;
255
+ const totalsText = hasTotals ? formatUsage(totals) : "";
256
+
257
+ return html`
258
+ <div class="text-xs text-muted-foreground flex justify-between items-center h-5">
259
+ <div class="flex items-center gap-1">
260
+ ${this.showThemeToggle ? html`<theme-toggle></theme-toggle>` : html``}
261
+ </div>
262
+ <div class="flex ml-auto items-center gap-3">${totalsText ? html`<span>${totalsText}</span>` : ""}</div>
263
+ </div>
264
+ `;
265
+ }
266
+
267
+ override render() {
268
+ if (!this.session)
269
+ return html`<div class="p-4 text-center text-muted-foreground">${i18n("No session set")}</div>`;
270
+
271
+ const session = this.session;
272
+ const state = this.session.state;
273
+ return html`
274
+ <div class="flex flex-col h-full bg-background text-foreground">
275
+ <!-- Messages Area -->
276
+ <div class="flex-1 overflow-y-auto">
277
+ <div class="max-w-3xl mx-auto p-4 pb-0">${this.renderMessages()}</div>
278
+ </div>
279
+
280
+ <!-- Input Area -->
281
+ <div class="shrink-0">
282
+ <div class="max-w-3xl mx-auto px-2">
283
+ <message-editor
284
+ .isStreaming=${state.isStreaming}
285
+ .currentModel=${state.model}
286
+ .thinkingLevel=${state.thinkingLevel}
287
+ .showAttachmentButton=${this.enableAttachments}
288
+ .showModelSelector=${this.enableModelSelector}
289
+ .showThinking=${this.enableThinking}
290
+ .onSend=${(input: string, attachments: Attachment[]) => {
291
+ this.sendMessage(input, attachments);
292
+ }}
293
+ .onAbort=${() => session.abort()}
294
+ .onModelSelect=${() => {
295
+ ModelSelector.open(state.model, (model) => session.setModel(model));
296
+ }}
297
+ .onThinkingChange=${
298
+ this.enableThinking
299
+ ? (level: "off" | "minimal" | "low" | "medium" | "high") => {
300
+ session.setThinkingLevel(level);
301
+ }
302
+ : undefined
303
+ }
304
+ ></message-editor>
305
+ ${this.renderStats()}
306
+ </div>
307
+ </div>
308
+ </div>
309
+ `;
310
+ }
311
+ }
312
+
313
+ // Register custom element with guard
314
+ if (!customElements.get("agent-interface")) {
315
+ customElements.define("agent-interface", AgentInterface);
316
+ }
@@ -0,0 +1,112 @@
1
+ import { html, icon } from "@mariozechner/mini-lit";
2
+ import { LitElement } from "lit";
3
+ import { customElement, property } from "lit/decorators.js";
4
+ import { FileSpreadsheet, FileText, X } from "lucide";
5
+ import { AttachmentOverlay } from "../dialogs/AttachmentOverlay.js";
6
+ import type { Attachment } from "../utils/attachment-utils.js";
7
+ import { i18n } from "../utils/i18n.js";
8
+
9
+ @customElement("attachment-tile")
10
+ export class AttachmentTile extends LitElement {
11
+ @property({ type: Object }) attachment!: Attachment;
12
+ @property({ type: Boolean }) showDelete = false;
13
+ @property() onDelete?: () => void;
14
+
15
+ protected override createRenderRoot(): HTMLElement | DocumentFragment {
16
+ return this;
17
+ }
18
+
19
+ override connectedCallback(): void {
20
+ super.connectedCallback();
21
+ this.style.display = "block";
22
+ this.classList.add("max-h-16");
23
+ }
24
+
25
+ private handleClick = () => {
26
+ AttachmentOverlay.open(this.attachment);
27
+ };
28
+
29
+ override render() {
30
+ const hasPreview = !!this.attachment.preview;
31
+ const isImage = this.attachment.type === "image";
32
+ const isPdf = this.attachment.mimeType === "application/pdf";
33
+ const isDocx =
34
+ this.attachment.mimeType?.includes("wordprocessingml") ||
35
+ this.attachment.fileName.toLowerCase().endsWith(".docx");
36
+ const isPptx =
37
+ this.attachment.mimeType?.includes("presentationml") ||
38
+ this.attachment.fileName.toLowerCase().endsWith(".pptx");
39
+ const isExcel =
40
+ this.attachment.mimeType?.includes("spreadsheetml") ||
41
+ this.attachment.fileName.toLowerCase().endsWith(".xlsx") ||
42
+ this.attachment.fileName.toLowerCase().endsWith(".xls");
43
+
44
+ // Choose the appropriate icon
45
+ const getDocumentIcon = () => {
46
+ if (isExcel) return icon(FileSpreadsheet, "md");
47
+ return icon(FileText, "md");
48
+ };
49
+
50
+ return html`
51
+ <div class="relative group inline-block">
52
+ ${
53
+ hasPreview
54
+ ? html`
55
+ <div class="relative">
56
+ <img
57
+ src="data:${isImage ? this.attachment.mimeType : "image/png"};base64,${this.attachment.preview}"
58
+ class="w-16 h-16 object-cover rounded-lg border border-input cursor-pointer hover:opacity-80 transition-opacity"
59
+ alt="${this.attachment.fileName}"
60
+ title="${this.attachment.fileName}"
61
+ @click=${this.handleClick}
62
+ />
63
+ ${
64
+ isPdf
65
+ ? html`
66
+ <!-- PDF badge overlay -->
67
+ <div class="absolute bottom-0 left-0 right-0 bg-background/90 px-1 py-0.5 rounded-b-lg">
68
+ <div class="text-[10px] text-muted-foreground text-center font-medium">${i18n("PDF")}</div>
69
+ </div>
70
+ `
71
+ : ""
72
+ }
73
+ </div>
74
+ `
75
+ : html`
76
+ <!-- Fallback: document icon + filename -->
77
+ <div
78
+ class="w-16 h-16 rounded-lg border border-input cursor-pointer hover:opacity-80 transition-opacity bg-muted text-muted-foreground flex flex-col items-center justify-center p-2"
79
+ @click=${this.handleClick}
80
+ title="${this.attachment.fileName}"
81
+ >
82
+ ${getDocumentIcon()}
83
+ <div class="text-[10px] text-center truncate w-full">
84
+ ${
85
+ this.attachment.fileName.length > 10
86
+ ? this.attachment.fileName.substring(0, 8) + "..."
87
+ : this.attachment.fileName
88
+ }
89
+ </div>
90
+ </div>
91
+ `
92
+ }
93
+ ${
94
+ this.showDelete
95
+ ? html`
96
+ <button
97
+ @click=${(e: Event) => {
98
+ e.stopPropagation();
99
+ this.onDelete?.();
100
+ }}
101
+ class="absolute -top-1 -right-1 w-5 h-5 bg-background hover:bg-muted text-muted-foreground hover:text-foreground rounded-full flex items-center justify-center opacity-100 hover:opacity-100 [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-hover:opacity-100 transition-opacity border border-input shadow-sm"
102
+ title="${i18n("Remove")}"
103
+ >
104
+ ${icon(X, "xs")}
105
+ </button>
106
+ `
107
+ : ""
108
+ }
109
+ </div>
110
+ `;
111
+ }
112
+ }
@@ -0,0 +1,67 @@
1
+ import { html, icon } from "@mariozechner/mini-lit";
2
+ import { LitElement } from "lit";
3
+ import { property, state } from "lit/decorators.js";
4
+ import { Check, Copy } from "lucide";
5
+ import { i18n } from "../utils/i18n.js";
6
+
7
+ export class ConsoleBlock extends LitElement {
8
+ @property() content: string = "";
9
+ @state() private copied = false;
10
+
11
+ protected override createRenderRoot(): HTMLElement | DocumentFragment {
12
+ return this;
13
+ }
14
+
15
+ override connectedCallback(): void {
16
+ super.connectedCallback();
17
+ this.style.display = "block";
18
+ }
19
+
20
+ private async copy() {
21
+ try {
22
+ await navigator.clipboard.writeText(this.content || "");
23
+ this.copied = true;
24
+ setTimeout(() => {
25
+ this.copied = false;
26
+ }, 1500);
27
+ } catch (e) {
28
+ console.error("Copy failed", e);
29
+ }
30
+ }
31
+
32
+ override updated() {
33
+ // Auto-scroll to bottom on content changes
34
+ const container = this.querySelector(".console-scroll") as HTMLElement | null;
35
+ if (container) {
36
+ container.scrollTop = container.scrollHeight;
37
+ }
38
+ }
39
+
40
+ override render() {
41
+ return html`
42
+ <div class="border border-border rounded-lg overflow-hidden">
43
+ <div class="flex items-center justify-between px-3 py-1.5 bg-muted border-b border-border">
44
+ <span class="text-xs text-muted-foreground font-mono">${i18n("console")}</span>
45
+ <button
46
+ @click=${() => this.copy()}
47
+ class="flex items-center gap-1 px-2 py-0.5 text-xs rounded hover:bg-accent text-muted-foreground hover:text-accent-foreground transition-colors"
48
+ title="${i18n("Copy output")}"
49
+ >
50
+ ${this.copied ? icon(Check, "sm") : icon(Copy, "sm")}
51
+ ${this.copied ? html`<span>${i18n("Copied!")}</span>` : ""}
52
+ </button>
53
+ </div>
54
+ <div class="console-scroll overflow-auto max-h-64">
55
+ <pre class="!bg-background !border-0 !rounded-none m-0 p-3 text-xs text-foreground font-mono whitespace-pre-wrap">
56
+ ${this.content || ""}</pre
57
+ >
58
+ </div>
59
+ </div>
60
+ `;
61
+ }
62
+ }
63
+
64
+ // Register custom element
65
+ if (!customElements.get("console-block")) {
66
+ customElements.define("console-block", ConsoleBlock);
67
+ }
@@ -0,0 +1,112 @@
1
+ import { type BaseComponentProps, fc, html } from "@mariozechner/mini-lit";
2
+ import { type Ref, ref } from "lit/directives/ref.js";
3
+ import { i18n } from "../utils/i18n.js";
4
+
5
+ export type InputType = "text" | "email" | "password" | "number" | "url" | "tel" | "search";
6
+ export type InputSize = "sm" | "md" | "lg";
7
+
8
+ export interface InputProps extends BaseComponentProps {
9
+ type?: InputType;
10
+ size?: InputSize;
11
+ value?: string;
12
+ placeholder?: string;
13
+ label?: string;
14
+ error?: string;
15
+ disabled?: boolean;
16
+ required?: boolean;
17
+ name?: string;
18
+ autocomplete?: string;
19
+ min?: number;
20
+ max?: number;
21
+ step?: number;
22
+ inputRef?: Ref<HTMLInputElement>;
23
+ onInput?: (e: Event) => void;
24
+ onChange?: (e: Event) => void;
25
+ onKeyDown?: (e: KeyboardEvent) => void;
26
+ onKeyUp?: (e: KeyboardEvent) => void;
27
+ }
28
+
29
+ export const Input = fc<InputProps>(
30
+ ({
31
+ type = "text",
32
+ size = "md",
33
+ value = "",
34
+ placeholder = "",
35
+ label = "",
36
+ error = "",
37
+ disabled = false,
38
+ required = false,
39
+ name = "",
40
+ autocomplete = "",
41
+ min,
42
+ max,
43
+ step,
44
+ inputRef,
45
+ onInput,
46
+ onChange,
47
+ onKeyDown,
48
+ onKeyUp,
49
+ className = "",
50
+ }) => {
51
+ const sizeClasses = {
52
+ sm: "h-8 px-3 py-1 text-sm",
53
+ md: "h-9 px-3 py-1 text-sm md:text-sm",
54
+ lg: "h-10 px-4 py-1 text-base",
55
+ };
56
+
57
+ const baseClasses =
58
+ "flex w-full min-w-0 rounded-md border bg-transparent text-foreground shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium";
59
+ const interactionClasses =
60
+ "placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground";
61
+ const focusClasses = "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]";
62
+ const darkClasses = "dark:bg-input/30";
63
+ const stateClasses = error
64
+ ? "border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40"
65
+ : "border-input";
66
+ const disabledClasses = "disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50";
67
+
68
+ const handleInput = (e: Event) => {
69
+ onInput?.(e);
70
+ };
71
+
72
+ const handleChange = (e: Event) => {
73
+ onChange?.(e);
74
+ };
75
+
76
+ return html`
77
+ <div class="flex flex-col gap-1.5 ${className}">
78
+ ${
79
+ label
80
+ ? html`
81
+ <label class="text-sm font-medium text-foreground">
82
+ ${label} ${required ? html`<span class="text-destructive">${i18n("*")}</span>` : ""}
83
+ </label>
84
+ `
85
+ : ""
86
+ }
87
+ <input
88
+ type="${type}"
89
+ class="${baseClasses} ${
90
+ sizeClasses[size]
91
+ } ${interactionClasses} ${focusClasses} ${darkClasses} ${stateClasses} ${disabledClasses}"
92
+ .value=${value}
93
+ placeholder="${placeholder}"
94
+ ?disabled=${disabled}
95
+ ?required=${required}
96
+ ?aria-invalid=${!!error}
97
+ name="${name}"
98
+ autocomplete="${autocomplete}"
99
+ min="${min ?? ""}"
100
+ max="${max ?? ""}"
101
+ step="${step ?? ""}"
102
+ @input=${handleInput}
103
+ @change=${handleChange}
104
+ @keydown=${onKeyDown}
105
+ @keyup=${onKeyUp}
106
+ ${inputRef ? ref(inputRef) : ""}
107
+ />
108
+ ${error ? html`<span class="text-sm text-destructive">${error}</span>` : ""}
109
+ </div>
110
+ `;
111
+ },
112
+ );