@mariozechner/pi-web-ui 0.5.48 → 0.7.1

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 (222) hide show
  1. package/dist/ChatPanel.d.ts +1 -0
  2. package/dist/ChatPanel.d.ts.map +1 -1
  3. package/dist/ChatPanel.js +3 -2
  4. package/dist/ChatPanel.js.map +1 -1
  5. package/dist/agent/transports/ProviderTransport.d.ts +1 -1
  6. package/dist/agent/transports/ProviderTransport.d.ts.map +1 -1
  7. package/dist/agent/transports/ProviderTransport.js +5 -10
  8. package/dist/agent/transports/ProviderTransport.js.map +1 -1
  9. package/dist/app.css +4188 -2
  10. package/dist/components/AgentInterface.d.ts +1 -0
  11. package/dist/components/AgentInterface.d.ts.map +1 -1
  12. package/dist/components/AgentInterface.js +13 -3
  13. package/dist/components/AgentInterface.js.map +1 -1
  14. package/dist/components/AttachmentTile.d.ts.map +1 -1
  15. package/dist/components/AttachmentTile.js +2 -1
  16. package/dist/components/AttachmentTile.js.map +1 -1
  17. package/dist/components/ConsoleBlock.d.ts.map +1 -1
  18. package/dist/components/ConsoleBlock.js +2 -1
  19. package/dist/components/ConsoleBlock.js.map +1 -1
  20. package/dist/components/CustomProviderCard.d.ts +17 -0
  21. package/dist/components/CustomProviderCard.d.ts.map +1 -0
  22. package/dist/components/CustomProviderCard.js +110 -0
  23. package/dist/components/CustomProviderCard.js.map +1 -0
  24. package/dist/components/Input.d.ts +2 -2
  25. package/dist/components/Input.d.ts.map +1 -1
  26. package/dist/components/Input.js +2 -1
  27. package/dist/components/Input.js.map +1 -1
  28. package/dist/components/MessageEditor.d.ts +1 -3
  29. package/dist/components/MessageEditor.d.ts.map +1 -1
  30. package/dist/components/MessageEditor.js +6 -31
  31. package/dist/components/MessageEditor.js.map +1 -1
  32. package/dist/components/MessageList.d.ts +1 -0
  33. package/dist/components/MessageList.d.ts.map +1 -1
  34. package/dist/components/MessageList.js +6 -3
  35. package/dist/components/MessageList.js.map +1 -1
  36. package/dist/components/Messages.d.ts +2 -0
  37. package/dist/components/Messages.d.ts.map +1 -1
  38. package/dist/components/Messages.js +25 -14
  39. package/dist/components/Messages.js.map +1 -1
  40. package/dist/components/ProviderKeyInput.d.ts +1 -1
  41. package/dist/components/ProviderKeyInput.d.ts.map +1 -1
  42. package/dist/components/ProviderKeyInput.js +22 -36
  43. package/dist/components/ProviderKeyInput.js.map +1 -1
  44. package/dist/components/StreamingMessageContainer.d.ts +1 -0
  45. package/dist/components/StreamingMessageContainer.d.ts.map +1 -1
  46. package/dist/components/StreamingMessageContainer.js +5 -2
  47. package/dist/components/StreamingMessageContainer.js.map +1 -1
  48. package/dist/components/ThinkingBlock.d.ts +11 -0
  49. package/dist/components/ThinkingBlock.d.ts.map +1 -0
  50. package/dist/components/ThinkingBlock.js +58 -0
  51. package/dist/components/ThinkingBlock.js.map +1 -0
  52. package/dist/dialogs/ApiKeyPromptDialog.d.ts +1 -1
  53. package/dist/dialogs/ApiKeyPromptDialog.d.ts.map +1 -1
  54. package/dist/dialogs/ApiKeyPromptDialog.js +3 -1
  55. package/dist/dialogs/ApiKeyPromptDialog.js.map +1 -1
  56. package/dist/dialogs/AttachmentOverlay.d.ts.map +1 -1
  57. package/dist/dialogs/AttachmentOverlay.js +3 -2
  58. package/dist/dialogs/AttachmentOverlay.js.map +1 -1
  59. package/dist/dialogs/CustomProviderDialog.d.ts +25 -0
  60. package/dist/dialogs/CustomProviderDialog.d.ts.map +1 -0
  61. package/dist/dialogs/CustomProviderDialog.js +270 -0
  62. package/dist/dialogs/CustomProviderDialog.js.map +1 -0
  63. package/dist/dialogs/ModelSelector.d.ts +6 -6
  64. package/dist/dialogs/ModelSelector.d.ts.map +1 -1
  65. package/dist/dialogs/ModelSelector.js +60 -74
  66. package/dist/dialogs/ModelSelector.js.map +1 -1
  67. package/dist/dialogs/PersistentStorageDialog.d.ts +1 -1
  68. package/dist/dialogs/PersistentStorageDialog.d.ts.map +1 -1
  69. package/dist/dialogs/PersistentStorageDialog.js +4 -1
  70. package/dist/dialogs/PersistentStorageDialog.js.map +1 -1
  71. package/dist/dialogs/ProvidersModelsTab.d.ts +20 -0
  72. package/dist/dialogs/ProvidersModelsTab.d.ts.map +1 -0
  73. package/dist/dialogs/ProvidersModelsTab.js +191 -0
  74. package/dist/dialogs/ProvidersModelsTab.js.map +1 -0
  75. package/dist/dialogs/SessionListDialog.d.ts +1 -1
  76. package/dist/dialogs/SessionListDialog.d.ts.map +1 -1
  77. package/dist/dialogs/SessionListDialog.js +3 -1
  78. package/dist/dialogs/SessionListDialog.js.map +1 -1
  79. package/dist/dialogs/SettingsDialog.d.ts +1 -2
  80. package/dist/dialogs/SettingsDialog.d.ts.map +1 -1
  81. package/dist/dialogs/SettingsDialog.js +10 -3
  82. package/dist/dialogs/SettingsDialog.js.map +1 -1
  83. package/dist/index.d.ts +4 -0
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +3 -0
  86. package/dist/index.js.map +1 -1
  87. package/dist/storage/app-storage.d.ts +3 -1
  88. package/dist/storage/app-storage.d.ts.map +1 -1
  89. package/dist/storage/app-storage.js +2 -1
  90. package/dist/storage/app-storage.js.map +1 -1
  91. package/dist/storage/stores/custom-providers-store.d.ts +25 -0
  92. package/dist/storage/stores/custom-providers-store.d.ts.map +1 -0
  93. package/dist/storage/stores/custom-providers-store.js +35 -0
  94. package/dist/storage/stores/custom-providers-store.js.map +1 -0
  95. package/dist/storage/stores/sessions-store.d.ts.map +1 -1
  96. package/dist/storage/stores/sessions-store.js +0 -1
  97. package/dist/storage/stores/sessions-store.js.map +1 -1
  98. package/dist/storage/types.d.ts +0 -2
  99. package/dist/storage/types.d.ts.map +1 -1
  100. package/dist/tools/artifacts/ArtifactPill.d.ts +1 -1
  101. package/dist/tools/artifacts/ArtifactPill.d.ts.map +1 -1
  102. package/dist/tools/artifacts/ArtifactPill.js +2 -1
  103. package/dist/tools/artifacts/ArtifactPill.js.map +1 -1
  104. package/dist/tools/artifacts/DocxArtifact.js +1 -1
  105. package/dist/tools/artifacts/DocxArtifact.js.map +1 -1
  106. package/dist/tools/artifacts/ExcelArtifact.js +1 -1
  107. package/dist/tools/artifacts/ExcelArtifact.js.map +1 -1
  108. package/dist/tools/artifacts/GenericArtifact.js +1 -1
  109. package/dist/tools/artifacts/GenericArtifact.js.map +1 -1
  110. package/dist/tools/artifacts/HtmlArtifact.d.ts.map +1 -1
  111. package/dist/tools/artifacts/HtmlArtifact.js +5 -1
  112. package/dist/tools/artifacts/HtmlArtifact.js.map +1 -1
  113. package/dist/tools/artifacts/ImageArtifact.js +1 -1
  114. package/dist/tools/artifacts/ImageArtifact.js.map +1 -1
  115. package/dist/tools/artifacts/MarkdownArtifact.d.ts.map +1 -1
  116. package/dist/tools/artifacts/MarkdownArtifact.js +3 -1
  117. package/dist/tools/artifacts/MarkdownArtifact.js.map +1 -1
  118. package/dist/tools/artifacts/PdfArtifact.js +1 -1
  119. package/dist/tools/artifacts/PdfArtifact.js.map +1 -1
  120. package/dist/tools/artifacts/SvgArtifact.d.ts.map +1 -1
  121. package/dist/tools/artifacts/SvgArtifact.js +3 -1
  122. package/dist/tools/artifacts/SvgArtifact.js.map +1 -1
  123. package/dist/tools/artifacts/TextArtifact.d.ts.map +1 -1
  124. package/dist/tools/artifacts/TextArtifact.js +2 -1
  125. package/dist/tools/artifacts/TextArtifact.js.map +1 -1
  126. package/dist/tools/artifacts/artifacts-tool-renderer.d.ts.map +1 -1
  127. package/dist/tools/artifacts/artifacts-tool-renderer.js +18 -8
  128. package/dist/tools/artifacts/artifacts-tool-renderer.js.map +1 -1
  129. package/dist/tools/artifacts/artifacts.d.ts.map +1 -1
  130. package/dist/tools/artifacts/artifacts.js +3 -2
  131. package/dist/tools/artifacts/artifacts.js.map +1 -1
  132. package/dist/tools/extract-document.d.ts.map +1 -1
  133. package/dist/tools/extract-document.js +78 -58
  134. package/dist/tools/extract-document.js.map +1 -1
  135. package/dist/tools/javascript-repl.d.ts.map +1 -1
  136. package/dist/tools/javascript-repl.js +7 -3
  137. package/dist/tools/javascript-repl.js.map +1 -1
  138. package/dist/tools/renderer-registry.d.ts +1 -1
  139. package/dist/tools/renderer-registry.d.ts.map +1 -1
  140. package/dist/tools/renderer-registry.js +20 -6
  141. package/dist/tools/renderer-registry.js.map +1 -1
  142. package/dist/tools/renderers/BashRenderer.d.ts.map +1 -1
  143. package/dist/tools/renderers/BashRenderer.js +5 -2
  144. package/dist/tools/renderers/BashRenderer.js.map +1 -1
  145. package/dist/tools/renderers/CalculateRenderer.d.ts.map +1 -1
  146. package/dist/tools/renderers/CalculateRenderer.js +5 -2
  147. package/dist/tools/renderers/CalculateRenderer.js.map +1 -1
  148. package/dist/tools/renderers/DefaultRenderer.d.ts.map +1 -1
  149. package/dist/tools/renderers/DefaultRenderer.js +5 -2
  150. package/dist/tools/renderers/DefaultRenderer.js.map +1 -1
  151. package/dist/tools/renderers/GetCurrentTimeRenderer.d.ts.map +1 -1
  152. package/dist/tools/renderers/GetCurrentTimeRenderer.js +9 -3
  153. package/dist/tools/renderers/GetCurrentTimeRenderer.js.map +1 -1
  154. package/dist/utils/auth-token.js +1 -1
  155. package/dist/utils/auth-token.js.map +1 -1
  156. package/dist/utils/i18n.d.ts +105 -3
  157. package/dist/utils/i18n.d.ts.map +1 -1
  158. package/dist/utils/i18n.js +72 -2
  159. package/dist/utils/i18n.js.map +1 -1
  160. package/dist/utils/model-discovery.d.ts +38 -0
  161. package/dist/utils/model-discovery.d.ts.map +1 -0
  162. package/dist/utils/model-discovery.js +243 -0
  163. package/dist/utils/model-discovery.js.map +1 -0
  164. package/dist/utils/proxy-utils.d.ts +37 -0
  165. package/dist/utils/proxy-utils.d.ts.map +1 -0
  166. package/dist/utils/proxy-utils.js +97 -0
  167. package/dist/utils/proxy-utils.js.map +1 -0
  168. package/example/package.json +2 -2
  169. package/example/src/custom-messages.ts +1 -1
  170. package/example/src/main.ts +17 -6
  171. package/package.json +9 -8
  172. package/src/ChatPanel.ts +4 -2
  173. package/src/agent/transports/ProviderTransport.ts +5 -10
  174. package/src/app.css +24 -0
  175. package/src/components/AgentInterface.ts +14 -3
  176. package/src/components/AttachmentTile.ts +2 -1
  177. package/src/components/ConsoleBlock.ts +2 -1
  178. package/src/components/CustomProviderCard.ts +100 -0
  179. package/src/components/Input.ts +2 -1
  180. package/src/components/MessageEditor.ts +6 -33
  181. package/src/components/MessageList.ts +4 -3
  182. package/src/components/Messages.ts +32 -20
  183. package/src/components/ProviderKeyInput.ts +19 -38
  184. package/src/components/StreamingMessageContainer.ts +3 -2
  185. package/src/components/ThinkingBlock.ts +43 -0
  186. package/src/dialogs/ApiKeyPromptDialog.ts +3 -1
  187. package/src/dialogs/AttachmentOverlay.ts +3 -2
  188. package/src/dialogs/CustomProviderDialog.ts +274 -0
  189. package/src/dialogs/ModelSelector.ts +61 -75
  190. package/src/dialogs/PersistentStorageDialog.ts +4 -1
  191. package/src/dialogs/ProvidersModelsTab.ts +212 -0
  192. package/src/dialogs/SessionListDialog.ts +3 -1
  193. package/src/dialogs/SettingsDialog.ts +10 -13
  194. package/src/index.ts +8 -0
  195. package/src/storage/app-storage.ts +4 -0
  196. package/src/storage/stores/custom-providers-store.ts +62 -0
  197. package/src/storage/stores/sessions-store.ts +0 -1
  198. package/src/storage/types.ts +0 -3
  199. package/src/tools/artifacts/ArtifactPill.ts +2 -1
  200. package/src/tools/artifacts/DocxArtifact.ts +1 -1
  201. package/src/tools/artifacts/ExcelArtifact.ts +1 -1
  202. package/src/tools/artifacts/GenericArtifact.ts +1 -1
  203. package/src/tools/artifacts/HtmlArtifact.ts +5 -1
  204. package/src/tools/artifacts/ImageArtifact.ts +1 -1
  205. package/src/tools/artifacts/MarkdownArtifact.ts +3 -1
  206. package/src/tools/artifacts/PdfArtifact.ts +1 -1
  207. package/src/tools/artifacts/SvgArtifact.ts +3 -1
  208. package/src/tools/artifacts/TextArtifact.ts +2 -1
  209. package/src/tools/artifacts/artifacts-tool-renderer.ts +20 -8
  210. package/src/tools/artifacts/artifacts.ts +3 -2
  211. package/src/tools/extract-document.ts +82 -61
  212. package/src/tools/javascript-repl.ts +8 -3
  213. package/src/tools/renderer-registry.ts +20 -6
  214. package/src/tools/renderers/BashRenderer.ts +6 -2
  215. package/src/tools/renderers/CalculateRenderer.ts +6 -2
  216. package/src/tools/renderers/DefaultRenderer.ts +6 -2
  217. package/src/tools/renderers/GetCurrentTimeRenderer.ts +11 -3
  218. package/src/utils/auth-token.ts +1 -1
  219. package/src/utils/i18n.ts +120 -5
  220. package/src/utils/model-discovery.ts +277 -0
  221. package/src/utils/proxy-utils.ts +112 -0
  222. package/example/package-lock.json +0 -1965
@@ -0,0 +1,274 @@
1
+ import { i18n } from "@mariozechner/mini-lit";
2
+ import { Button } from "@mariozechner/mini-lit/dist/Button.js";
3
+ import { DialogBase } from "@mariozechner/mini-lit/dist/DialogBase.js";
4
+ import { Input } from "@mariozechner/mini-lit/dist/Input.js";
5
+ import { Label } from "@mariozechner/mini-lit/dist/Label.js";
6
+ import { Select } from "@mariozechner/mini-lit/dist/Select.js";
7
+ import type { Model } from "@mariozechner/pi-ai";
8
+ import { html, type TemplateResult } from "lit";
9
+ import { state } from "lit/decorators.js";
10
+ import { getAppStorage } from "../storage/app-storage.js";
11
+ import type { CustomProvider, CustomProviderType } from "../storage/stores/custom-providers-store.js";
12
+ import { discoverModels } from "../utils/model-discovery.js";
13
+
14
+ export class CustomProviderDialog extends DialogBase {
15
+ private provider?: CustomProvider;
16
+ private initialType?: CustomProviderType;
17
+ private onSaveCallback?: () => void;
18
+
19
+ @state() private name = "";
20
+ @state() private type: CustomProviderType = "openai-completions";
21
+ @state() private baseUrl = "";
22
+ @state() private apiKey = "";
23
+ @state() private testing = false;
24
+ @state() private testError = "";
25
+ @state() private discoveredModels: Model<any>[] = [];
26
+
27
+ protected modalWidth = "min(800px, 90vw)";
28
+ protected modalHeight = "min(700px, 90vh)";
29
+
30
+ static async open(
31
+ provider: CustomProvider | undefined,
32
+ initialType: CustomProviderType | undefined,
33
+ onSave?: () => void,
34
+ ) {
35
+ const dialog = new CustomProviderDialog();
36
+ dialog.provider = provider;
37
+ dialog.initialType = initialType;
38
+ dialog.onSaveCallback = onSave;
39
+ document.body.appendChild(dialog);
40
+ dialog.initializeFromProvider();
41
+ dialog.open();
42
+ dialog.requestUpdate();
43
+ }
44
+
45
+ private initializeFromProvider() {
46
+ if (this.provider) {
47
+ this.name = this.provider.name;
48
+ this.type = this.provider.type;
49
+ this.baseUrl = this.provider.baseUrl;
50
+ this.apiKey = this.provider.apiKey || "";
51
+ this.discoveredModels = this.provider.models || [];
52
+ } else {
53
+ this.name = "";
54
+ this.type = this.initialType || "openai-completions";
55
+ this.baseUrl = "";
56
+ this.updateDefaultBaseUrl();
57
+ this.apiKey = "";
58
+ this.discoveredModels = [];
59
+ }
60
+ this.testError = "";
61
+ this.testing = false;
62
+ }
63
+
64
+ private updateDefaultBaseUrl() {
65
+ if (this.baseUrl) return;
66
+
67
+ const defaults: Record<string, string> = {
68
+ ollama: "http://localhost:11434",
69
+ "llama.cpp": "http://localhost:8080",
70
+ vllm: "http://localhost:8000",
71
+ lmstudio: "http://localhost:1234",
72
+ "openai-completions": "",
73
+ "openai-responses": "",
74
+ "anthropic-messages": "",
75
+ };
76
+
77
+ this.baseUrl = defaults[this.type] || "";
78
+ }
79
+
80
+ private isAutoDiscoveryType(): boolean {
81
+ return this.type === "ollama" || this.type === "llama.cpp" || this.type === "vllm" || this.type === "lmstudio";
82
+ }
83
+
84
+ private async testConnection() {
85
+ if (!this.isAutoDiscoveryType()) return;
86
+
87
+ this.testing = true;
88
+ this.testError = "";
89
+ this.discoveredModels = [];
90
+
91
+ try {
92
+ const models = await discoverModels(
93
+ this.type as "ollama" | "llama.cpp" | "vllm" | "lmstudio",
94
+ this.baseUrl,
95
+ this.apiKey || undefined,
96
+ );
97
+
98
+ this.discoveredModels = models.map((model) => ({
99
+ ...model,
100
+ provider: this.name || this.type,
101
+ }));
102
+
103
+ this.testError = "";
104
+ } catch (error) {
105
+ this.testError = error instanceof Error ? error.message : String(error);
106
+ this.discoveredModels = [];
107
+ } finally {
108
+ this.testing = false;
109
+ this.requestUpdate();
110
+ }
111
+ }
112
+
113
+ private async save() {
114
+ if (!this.name || !this.baseUrl) {
115
+ alert(i18n("Please fill in all required fields"));
116
+ return;
117
+ }
118
+
119
+ try {
120
+ const storage = getAppStorage();
121
+
122
+ const provider: CustomProvider = {
123
+ id: this.provider?.id || crypto.randomUUID(),
124
+ name: this.name,
125
+ type: this.type,
126
+ baseUrl: this.baseUrl,
127
+ apiKey: this.apiKey || undefined,
128
+ models: this.isAutoDiscoveryType() ? undefined : this.provider?.models || [],
129
+ };
130
+
131
+ await storage.customProviders.set(provider);
132
+
133
+ if (this.onSaveCallback) {
134
+ this.onSaveCallback();
135
+ }
136
+ this.close();
137
+ } catch (error) {
138
+ console.error("Failed to save provider:", error);
139
+ alert(i18n("Failed to save provider"));
140
+ }
141
+ }
142
+
143
+ protected override renderContent(): TemplateResult {
144
+ const providerTypes = [
145
+ { value: "ollama", label: "Ollama (auto-discovery)" },
146
+ { value: "llama.cpp", label: "llama.cpp (auto-discovery)" },
147
+ { value: "vllm", label: "vLLM (auto-discovery)" },
148
+ { value: "lmstudio", label: "LM Studio (auto-discovery)" },
149
+ { value: "openai-completions", label: "OpenAI Completions Compatible" },
150
+ { value: "openai-responses", label: "OpenAI Responses Compatible" },
151
+ { value: "anthropic-messages", label: "Anthropic Messages Compatible" },
152
+ ];
153
+
154
+ return html`
155
+ <div class="flex flex-col h-full overflow-hidden">
156
+ <div class="p-6 flex-shrink-0 border-b border-border">
157
+ <h2 class="text-lg font-semibold text-foreground">
158
+ ${this.provider ? i18n("Edit Provider") : i18n("Add Provider")}
159
+ </h2>
160
+ </div>
161
+
162
+ <div class="flex-1 overflow-y-auto p-6">
163
+ <div class="flex flex-col gap-4">
164
+ <div class="flex flex-col gap-2">
165
+ ${Label({ htmlFor: "provider-name", children: i18n("Provider Name") })}
166
+ ${Input({
167
+ value: this.name,
168
+ placeholder: i18n("e.g., My Ollama Server"),
169
+ onInput: (e: Event) => {
170
+ this.name = (e.target as HTMLInputElement).value;
171
+ this.requestUpdate();
172
+ },
173
+ })}
174
+ </div>
175
+
176
+ <div class="flex flex-col gap-2">
177
+ ${Label({ htmlFor: "provider-type", children: i18n("Provider Type") })}
178
+ ${Select({
179
+ value: this.type,
180
+ options: providerTypes.map((pt) => ({
181
+ value: pt.value,
182
+ label: pt.label,
183
+ })),
184
+ onChange: (value: string) => {
185
+ this.type = value as CustomProviderType;
186
+ this.baseUrl = "";
187
+ this.updateDefaultBaseUrl();
188
+ this.requestUpdate();
189
+ },
190
+ width: "100%",
191
+ })}
192
+ </div>
193
+
194
+ <div class="flex flex-col gap-2">
195
+ ${Label({ htmlFor: "base-url", children: i18n("Base URL") })}
196
+ ${Input({
197
+ value: this.baseUrl,
198
+ placeholder: i18n("e.g., http://localhost:11434"),
199
+ onInput: (e: Event) => {
200
+ this.baseUrl = (e.target as HTMLInputElement).value;
201
+ this.requestUpdate();
202
+ },
203
+ })}
204
+ </div>
205
+
206
+ <div class="flex flex-col gap-2">
207
+ ${Label({ htmlFor: "api-key", children: i18n("API Key (Optional)") })}
208
+ ${Input({
209
+ type: "password",
210
+ value: this.apiKey,
211
+ placeholder: i18n("Leave empty if not required"),
212
+ onInput: (e: Event) => {
213
+ this.apiKey = (e.target as HTMLInputElement).value;
214
+ this.requestUpdate();
215
+ },
216
+ })}
217
+ </div>
218
+
219
+ ${
220
+ this.isAutoDiscoveryType()
221
+ ? html`
222
+ <div class="flex flex-col gap-2">
223
+ ${Button({
224
+ onClick: () => this.testConnection(),
225
+ variant: "outline",
226
+ disabled: this.testing || !this.baseUrl,
227
+ children: this.testing ? i18n("Testing...") : i18n("Test Connection"),
228
+ })}
229
+ ${this.testError ? html` <div class="text-sm text-destructive">${this.testError}</div> ` : ""}
230
+ ${
231
+ this.discoveredModels.length > 0
232
+ ? html`
233
+ <div class="text-sm text-muted-foreground">
234
+ ${i18n("Discovered")} ${this.discoveredModels.length} ${i18n("models")}:
235
+ <ul class="list-disc list-inside mt-2">
236
+ ${this.discoveredModels.slice(0, 5).map((model) => html`<li>${model.name}</li>`)}
237
+ ${
238
+ this.discoveredModels.length > 5
239
+ ? html`<li>...${i18n("and")} ${this.discoveredModels.length - 5} ${i18n("more")}</li>`
240
+ : ""
241
+ }
242
+ </ul>
243
+ </div>
244
+ `
245
+ : ""
246
+ }
247
+ </div>
248
+ `
249
+ : html` <div class="text-sm text-muted-foreground">
250
+ ${i18n("For manual provider types, add models after saving the provider.")}
251
+ </div>`
252
+ }
253
+ </div>
254
+ </div>
255
+
256
+ <div class="p-6 flex-shrink-0 border-t border-border flex justify-end gap-2">
257
+ ${Button({
258
+ onClick: () => this.close(),
259
+ variant: "ghost",
260
+ children: i18n("Cancel"),
261
+ })}
262
+ ${Button({
263
+ onClick: () => this.save(),
264
+ variant: "default",
265
+ disabled: !this.name || !this.baseUrl,
266
+ children: i18n("Save"),
267
+ })}
268
+ </div>
269
+ </div>
270
+ `;
271
+ }
272
+ }
273
+
274
+ customElements.define("custom-provider-dialog", CustomProviderDialog);
@@ -1,14 +1,19 @@
1
- import { Badge, Button, DialogBase, DialogHeader, html, icon, type TemplateResult } from "@mariozechner/mini-lit";
2
- import type { Model } from "@mariozechner/pi-ai";
3
- import { MODELS } from "@mariozechner/pi-ai/dist/models.generated.js";
4
- import type { PropertyValues } from "lit";
1
+ import { icon } from "@mariozechner/mini-lit";
2
+ import { Badge } from "@mariozechner/mini-lit/dist/Badge.js";
3
+ import { Button } from "@mariozechner/mini-lit/dist/Button.js";
4
+ import { DialogHeader } from "@mariozechner/mini-lit/dist/Dialog.js";
5
+ import { DialogBase } from "@mariozechner/mini-lit/dist/DialogBase.js";
6
+ import { getModels, getProviders, type Model } from "@mariozechner/pi-ai";
7
+ import { html, type PropertyValues, type TemplateResult } from "lit";
5
8
  import { customElement, state } from "lit/decorators.js";
6
9
  import { createRef, ref } from "lit/directives/ref.js";
7
10
  import { Brain, Image as ImageIcon } from "lucide";
8
- import { Ollama } from "ollama/dist/browser.mjs";
9
11
  import { Input } from "../components/Input.js";
12
+ import { getAppStorage } from "../storage/app-storage.js";
13
+ import type { AutoDiscoveryProviderType } from "../storage/stores/custom-providers-store.js";
10
14
  import { formatModelCost } from "../utils/format.js";
11
15
  import { i18n } from "../utils/i18n.js";
16
+ import { discoverModels } from "../utils/model-discovery.js";
12
17
 
13
18
  @customElement("agent-model-selector")
14
19
  export class ModelSelector extends DialogBase {
@@ -16,10 +21,10 @@ export class ModelSelector extends DialogBase {
16
21
  @state() searchQuery = "";
17
22
  @state() filterThinking = false;
18
23
  @state() filterVision = false;
19
- @state() ollamaModels: Model<any>[] = [];
20
- @state() ollamaError: string | null = null;
24
+ @state() customProvidersLoading = false;
21
25
  @state() selectedIndex = 0;
22
26
  @state() private navigationMode: "mouse" | "keyboard" = "mouse";
27
+ @state() private customProviderModels: Model<any>[] = [];
23
28
 
24
29
  private onSelectCallback?: (model: Model<any>) => void;
25
30
  private scrollContainerRef = createRef<HTMLDivElement>();
@@ -33,7 +38,7 @@ export class ModelSelector extends DialogBase {
33
38
  selector.currentModel = currentModel;
34
39
  selector.onSelectCallback = onSelect;
35
40
  selector.open();
36
- selector.fetchOllamaModels();
41
+ selector.loadCustomProviders();
37
42
  }
38
43
 
39
44
  override async firstUpdated(changedProperties: PropertyValues): Promise<void> {
@@ -91,67 +96,50 @@ export class ModelSelector extends DialogBase {
91
96
  });
92
97
  }
93
98
 
94
- private async fetchOllamaModels() {
99
+ private async loadCustomProviders() {
100
+ this.customProvidersLoading = true;
101
+ const allCustomModels: Model<any>[] = [];
102
+
95
103
  try {
96
- // Create Ollama client
97
- const ollama = new Ollama({ host: "http://localhost:11434" });
104
+ const storage = getAppStorage();
105
+ const customProviders = await storage.customProviders.getAll();
98
106
 
99
- // Get list of available models
100
- const { models } = await ollama.list();
107
+ // Load models from custom providers
108
+ for (const provider of customProviders) {
109
+ const isAutoDiscovery: boolean =
110
+ provider.type === "ollama" ||
111
+ provider.type === "llama.cpp" ||
112
+ provider.type === "vllm" ||
113
+ provider.type === "lmstudio";
101
114
 
102
- // Fetch details for each model and convert to Model format
103
- const ollamaModelPromises: Promise<Model<any> | null>[] = models
104
- .map(async (model: any) => {
115
+ if (isAutoDiscovery) {
105
116
  try {
106
- // Get model details
107
- const details = await ollama.show({
108
- model: model.name,
109
- });
110
-
111
- // Some Ollama servers don't report capabilities; don't filter on them
112
-
113
- // Extract model info
114
- const modelInfo: any = details.model_info || {};
117
+ const models = await discoverModels(
118
+ provider.type as AutoDiscoveryProviderType,
119
+ provider.baseUrl,
120
+ provider.apiKey,
121
+ );
115
122
 
116
- // Get context window size - look for architecture-specific keys
117
- const architecture = modelInfo["general.architecture"] || "";
118
- const contextKey = `${architecture}.context_length`;
119
- const contextWindow = parseInt(modelInfo[contextKey] || "8192", 10);
120
- const maxTokens = 4096; // Default max output tokens
123
+ const modelsWithProvider = models.map((model) => ({
124
+ ...model,
125
+ provider: provider.name,
126
+ }));
121
127
 
122
- // Create Model object manually since ollama models aren't in MODELS constant
123
- const ollamaModel: Model<any> = {
124
- id: model.name,
125
- name: model.name,
126
- api: "openai-completions" as any,
127
- provider: "ollama",
128
- baseUrl: "http://localhost:11434/v1",
129
- reasoning: false,
130
- input: ["text"],
131
- cost: {
132
- input: 0,
133
- output: 0,
134
- cacheRead: 0,
135
- cacheWrite: 0,
136
- },
137
- contextWindow: contextWindow,
138
- maxTokens: maxTokens,
139
- };
140
-
141
- return ollamaModel;
142
- } catch (err) {
143
- console.error(`Failed to fetch details for model ${model.name}:`, err);
144
- return null;
128
+ allCustomModels.push(...modelsWithProvider);
129
+ } catch (error) {
130
+ console.debug(`Failed to load models from ${provider.name}:`, error);
145
131
  }
146
- })
147
- .filter((m: any) => m !== null);
148
-
149
- const results = await Promise.all(ollamaModelPromises);
150
- this.ollamaModels = results.filter((m): m is Model<any> => m !== null);
151
- } catch (err) {
152
- // Ollama not available or other error - silently ignore
153
- console.debug("Ollama not available:", err);
154
- this.ollamaError = err instanceof Error ? err.message : String(err);
132
+ } else if (provider.models) {
133
+ // Manual provider - models already defined
134
+ allCustomModels.push(...provider.models);
135
+ }
136
+ }
137
+ } catch (error) {
138
+ console.error("Failed to load custom providers:", error);
139
+ } finally {
140
+ this.customProviderModels = allCustomModels;
141
+ this.customProvidersLoading = false;
142
+ this.requestUpdate();
155
143
  }
156
144
  }
157
145
 
@@ -169,21 +157,20 @@ export class ModelSelector extends DialogBase {
169
157
  }
170
158
 
171
159
  private getFilteredModels(): Array<{ provider: string; id: string; model: any }> {
172
- // Collect all models from all providers
160
+ // Collect all models from known providers
173
161
  const allModels: Array<{ provider: string; id: string; model: any }> = [];
174
- for (const [provider, providerData] of Object.entries(MODELS)) {
175
- for (const [modelId, model] of Object.entries(providerData)) {
176
- allModels.push({ provider, id: modelId, model });
162
+ const knownProviders = getProviders();
163
+
164
+ for (const provider of knownProviders) {
165
+ const models = getModels(provider as any);
166
+ for (const model of models) {
167
+ allModels.push({ provider, id: model.id, model });
177
168
  }
178
169
  }
179
170
 
180
- // Add Ollama models
181
- for (const ollamaModel of this.ollamaModels) {
182
- allModels.push({
183
- id: ollamaModel.id,
184
- provider: "ollama",
185
- model: ollamaModel,
186
- });
171
+ // Add custom provider models
172
+ for (const model of this.customProviderModels) {
173
+ allModels.push({ provider: model.provider, id: model.id, model });
187
174
  }
188
175
 
189
176
  // Filter models based on search and capability filters
@@ -283,8 +270,7 @@ export class ModelSelector extends DialogBase {
283
270
  <!-- Scrollable model list -->
284
271
  <div class="flex-1 overflow-y-auto" ${ref(this.scrollContainerRef)}>
285
272
  ${filteredModels.map(({ provider, id, model }, index) => {
286
- // Check if this is the current model by comparing IDs
287
- const isCurrent = this.currentModel?.id === model.id;
273
+ const isCurrent = this.currentModel?.id === model.id && this.currentModel?.provider === model.provider;
288
274
  const isSelected = index === this.selectedIndex;
289
275
  return html`
290
276
  <div
@@ -1,4 +1,7 @@
1
- import { Button, DialogBase, DialogContent, DialogHeader, html } from "@mariozechner/mini-lit";
1
+ import { Button } from "@mariozechner/mini-lit/dist/Button.js";
2
+ import { DialogContent, DialogHeader } from "@mariozechner/mini-lit/dist/Dialog.js";
3
+ import { DialogBase } from "@mariozechner/mini-lit/dist/DialogBase.js";
4
+ import { html } from "lit";
2
5
  import { customElement, state } from "lit/decorators.js";
3
6
  import { i18n } from "../utils/i18n.js";
4
7
 
@@ -0,0 +1,212 @@
1
+ import { i18n } from "@mariozechner/mini-lit";
2
+ import { Select } from "@mariozechner/mini-lit/dist/Select.js";
3
+ import { getProviders } from "@mariozechner/pi-ai";
4
+ import { html, type TemplateResult } from "lit";
5
+ import { customElement, state } from "lit/decorators.js";
6
+ import "../components/CustomProviderCard.js";
7
+ import "../components/ProviderKeyInput.js";
8
+ import { getAppStorage } from "../storage/app-storage.js";
9
+ import type {
10
+ AutoDiscoveryProviderType,
11
+ CustomProvider,
12
+ CustomProviderType,
13
+ } from "../storage/stores/custom-providers-store.js";
14
+ import { discoverModels } from "../utils/model-discovery.js";
15
+ import { CustomProviderDialog } from "./CustomProviderDialog.js";
16
+ import { SettingsTab } from "./SettingsDialog.js";
17
+
18
+ @customElement("providers-models-tab")
19
+ export class ProvidersModelsTab extends SettingsTab {
20
+ @state() private customProviders: CustomProvider[] = [];
21
+ @state() private providerStatus: Map<
22
+ string,
23
+ { modelCount: number; status: "connected" | "disconnected" | "checking" }
24
+ > = new Map();
25
+
26
+ override async connectedCallback() {
27
+ super.connectedCallback();
28
+ await this.loadCustomProviders();
29
+ }
30
+
31
+ private async loadCustomProviders() {
32
+ try {
33
+ const storage = getAppStorage();
34
+ this.customProviders = await storage.customProviders.getAll();
35
+
36
+ // Check status for auto-discovery providers
37
+ for (const provider of this.customProviders) {
38
+ const isAutoDiscovery =
39
+ provider.type === "ollama" ||
40
+ provider.type === "llama.cpp" ||
41
+ provider.type === "vllm" ||
42
+ provider.type === "lmstudio";
43
+ if (isAutoDiscovery) {
44
+ this.checkProviderStatus(provider);
45
+ }
46
+ }
47
+ } catch (error) {
48
+ console.error("Failed to load custom providers:", error);
49
+ }
50
+ }
51
+
52
+ getTabName(): string {
53
+ return "Providers & Models";
54
+ }
55
+
56
+ private async checkProviderStatus(provider: CustomProvider) {
57
+ this.providerStatus.set(provider.id, { modelCount: 0, status: "checking" });
58
+ this.requestUpdate();
59
+
60
+ try {
61
+ const models = await discoverModels(
62
+ provider.type as AutoDiscoveryProviderType,
63
+ provider.baseUrl,
64
+ provider.apiKey,
65
+ );
66
+
67
+ this.providerStatus.set(provider.id, { modelCount: models.length, status: "connected" });
68
+ } catch (error) {
69
+ this.providerStatus.set(provider.id, { modelCount: 0, status: "disconnected" });
70
+ }
71
+ this.requestUpdate();
72
+ }
73
+
74
+ private renderKnownProviders(): TemplateResult {
75
+ const providers = getProviders();
76
+
77
+ return html`
78
+ <div class="flex flex-col gap-6">
79
+ <div>
80
+ <h3 class="text-sm font-semibold text-foreground mb-2">Cloud Providers</h3>
81
+ <p class="text-sm text-muted-foreground mb-4">
82
+ Cloud LLM providers with predefined models. API keys are stored locally in your browser.
83
+ </p>
84
+ </div>
85
+ <div class="flex flex-col gap-6">
86
+ ${providers.map((provider) => html` <provider-key-input .provider=${provider}></provider-key-input> `)}
87
+ </div>
88
+ </div>
89
+ `;
90
+ }
91
+
92
+ private renderCustomProviders(): TemplateResult {
93
+ const isAutoDiscovery = (type: string) =>
94
+ type === "ollama" || type === "llama.cpp" || type === "vllm" || type === "lmstudio";
95
+
96
+ return html`
97
+ <div class="flex flex-col gap-6">
98
+ <div class="flex items-center justify-between">
99
+ <div>
100
+ <h3 class="text-sm font-semibold text-foreground mb-2">Custom Providers</h3>
101
+ <p class="text-sm text-muted-foreground">
102
+ User-configured servers with auto-discovered or manually defined models.
103
+ </p>
104
+ </div>
105
+ ${Select({
106
+ placeholder: i18n("Add Provider"),
107
+ options: [
108
+ { value: "ollama", label: "Ollama" },
109
+ { value: "llama.cpp", label: "llama.cpp" },
110
+ { value: "vllm", label: "vLLM" },
111
+ { value: "lmstudio", label: "LM Studio" },
112
+ { value: "openai-completions", label: i18n("OpenAI Completions Compatible") },
113
+ { value: "openai-responses", label: i18n("OpenAI Responses Compatible") },
114
+ { value: "anthropic-messages", label: i18n("Anthropic Messages Compatible") },
115
+ ],
116
+ onChange: (value: string) => this.addCustomProvider(value as CustomProviderType),
117
+ variant: "outline",
118
+ size: "sm",
119
+ })}
120
+ </div>
121
+
122
+ ${
123
+ this.customProviders.length === 0
124
+ ? html`
125
+ <div class="text-sm text-muted-foreground text-center py-8">
126
+ No custom providers configured. Click 'Add Provider' to get started.
127
+ </div>
128
+ `
129
+ : html`
130
+ <div class="flex flex-col gap-4">
131
+ ${this.customProviders.map(
132
+ (provider) => html`
133
+ <custom-provider-card
134
+ .provider=${provider}
135
+ .isAutoDiscovery=${isAutoDiscovery(provider.type)}
136
+ .status=${this.providerStatus.get(provider.id)}
137
+ .onRefresh=${(p: CustomProvider) => this.refreshProvider(p)}
138
+ .onEdit=${(p: CustomProvider) => this.editProvider(p)}
139
+ .onDelete=${(p: CustomProvider) => this.deleteProvider(p)}
140
+ ></custom-provider-card>
141
+ `,
142
+ )}
143
+ </div>
144
+ `
145
+ }
146
+ </div>
147
+ `;
148
+ }
149
+
150
+ private async addCustomProvider(type: CustomProviderType) {
151
+ await CustomProviderDialog.open(undefined, type, async () => {
152
+ await this.loadCustomProviders();
153
+ this.requestUpdate();
154
+ });
155
+ }
156
+
157
+ private async editProvider(provider: CustomProvider) {
158
+ await CustomProviderDialog.open(provider, undefined, async () => {
159
+ await this.loadCustomProviders();
160
+ this.requestUpdate();
161
+ });
162
+ }
163
+
164
+ private async refreshProvider(provider: CustomProvider) {
165
+ this.providerStatus.set(provider.id, { modelCount: 0, status: "checking" });
166
+ this.requestUpdate();
167
+
168
+ try {
169
+ const models = await discoverModels(
170
+ provider.type as AutoDiscoveryProviderType,
171
+ provider.baseUrl,
172
+ provider.apiKey,
173
+ );
174
+
175
+ this.providerStatus.set(provider.id, { modelCount: models.length, status: "connected" });
176
+ this.requestUpdate();
177
+
178
+ console.log(`Refreshed ${models.length} models from ${provider.name}`);
179
+ } catch (error) {
180
+ this.providerStatus.set(provider.id, { modelCount: 0, status: "disconnected" });
181
+ this.requestUpdate();
182
+
183
+ console.error(`Failed to refresh provider ${provider.name}:`, error);
184
+ alert(`Failed to refresh provider: ${error instanceof Error ? error.message : String(error)}`);
185
+ }
186
+ }
187
+
188
+ private async deleteProvider(provider: CustomProvider) {
189
+ if (!confirm("Are you sure you want to delete this provider?")) {
190
+ return;
191
+ }
192
+
193
+ try {
194
+ const storage = getAppStorage();
195
+ await storage.customProviders.delete(provider.id);
196
+ await this.loadCustomProviders();
197
+ this.requestUpdate();
198
+ } catch (error) {
199
+ console.error("Failed to delete provider:", error);
200
+ }
201
+ }
202
+
203
+ render(): TemplateResult {
204
+ return html`
205
+ <div class="flex flex-col gap-8">
206
+ ${this.renderKnownProviders()}
207
+ <div class="border-t border-border"></div>
208
+ ${this.renderCustomProviders()}
209
+ </div>
210
+ `;
211
+ }
212
+ }