@ouraihub/aether-ui 0.0.0-stage → 0.1.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 (212) hide show
  1. package/LICENSE +789 -0
  2. package/fixtures/model-classify.json +42 -0
  3. package/fixtures/pricing-cost.json +56 -0
  4. package/fixtures/reasoning-infer.json +44 -0
  5. package/fixtures/sse/anthropic-messages/chinese.sse +24 -0
  6. package/fixtures/sse/anthropic-messages/multi-tool.sse +33 -0
  7. package/fixtures/sse/anthropic-messages/single-tool.sse +39 -0
  8. package/fixtures/sse/anthropic-messages/text.sse +27 -0
  9. package/fixtures/sse/anthropic-messages/truncated.sse +21 -0
  10. package/fixtures/sse/anthropic-messages/upstream-error.sse +12 -0
  11. package/fixtures/sse/expect.json +20 -0
  12. package/fixtures/sse/openai-chat/chinese.sse +14 -0
  13. package/fixtures/sse/openai-chat/multi-tool.sse +18 -0
  14. package/fixtures/sse/openai-chat/single-tool.sse +22 -0
  15. package/fixtures/sse/openai-chat/text.sse +14 -0
  16. package/fixtures/sse/openai-chat/truncated.sse +12 -0
  17. package/fixtures/sse/openai-chat/upstream-error.sse +6 -0
  18. package/fixtures/sse/openai-responses/chinese.sse +33 -0
  19. package/fixtures/sse/openai-responses/multi-tool.sse +36 -0
  20. package/fixtures/sse/openai-responses/single-tool.sse +27 -0
  21. package/fixtures/sse/openai-responses/text.sse +33 -0
  22. package/fixtures/sse/openai-responses/truncated.sse +30 -0
  23. package/fixtures/sse/openai-responses/upstream-error.sse +9 -0
  24. package/package.json +71 -3
  25. package/src/CopyInput.svelte +33 -0
  26. package/src/KitNotifier.svelte +33 -0
  27. package/src/chat/ApprovalCard.svelte +29 -0
  28. package/src/chat/ChatComposer.svelte +277 -0
  29. package/src/chat/ChatMessage.svelte +262 -0
  30. package/src/chat/ChatPanel.css +386 -0
  31. package/src/chat/ChatPanel.svelte +496 -0
  32. package/src/chat/ChatPanelApprovals.svelte +28 -0
  33. package/src/chat/ChatPanelComposer.svelte +317 -0
  34. package/src/chat/ChatPanelDialogs.svelte +242 -0
  35. package/src/chat/ChatPanelHeader.svelte +132 -0
  36. package/src/chat/ChatPanelMessages.svelte +166 -0
  37. package/src/chat/ChatPanelMode.svelte +190 -0
  38. package/src/chat/ChatPanelState.svelte.ts +1 -0
  39. package/src/chat/ChatPanelToolbar.svelte +425 -0
  40. package/src/chat/ChatSurface.svelte +168 -0
  41. package/src/chat/MessageItem.svelte +480 -0
  42. package/src/chat/ReasoningControl.svelte +286 -0
  43. package/src/chat/SkillPicker.svelte +133 -0
  44. package/src/chat/ToolCallItem.svelte +23 -0
  45. package/src/chat/UsageControl.svelte +216 -0
  46. package/src/chat/UsageDetails.svelte +76 -0
  47. package/src/chat/chat-attachments.svelte.ts +74 -0
  48. package/src/chat/chat-panel-history.svelte.ts +430 -0
  49. package/src/chat/chat-panel-props.ts +32 -0
  50. package/src/chat/index.ts +6 -0
  51. package/src/chat/run-session-history.ts +56 -0
  52. package/src/chat/run-session-types.ts +69 -0
  53. package/src/chat/run-session.svelte.ts +494 -0
  54. package/src/chat/test-support/chat-fixture.ts +178 -0
  55. package/src/chat/test-support/conversation-fixtures.ts +32 -0
  56. package/src/chat/test-support/run-session-fixture.ts +112 -0
  57. package/src/components/ui/alert/alert-action.svelte +20 -0
  58. package/src/components/ui/alert/alert-description.svelte +23 -0
  59. package/src/components/ui/alert/alert-title.svelte +23 -0
  60. package/src/components/ui/alert/alert.svelte +43 -0
  61. package/src/components/ui/alert/index.ts +17 -0
  62. package/src/components/ui/alert-dialog/alert-dialog-action.svelte +27 -0
  63. package/src/components/ui/alert-dialog/alert-dialog-cancel.svelte +27 -0
  64. package/src/components/ui/alert-dialog/alert-dialog-content.svelte +32 -0
  65. package/src/components/ui/alert-dialog/alert-dialog-description.svelte +17 -0
  66. package/src/components/ui/alert-dialog/alert-dialog-footer.svelte +23 -0
  67. package/src/components/ui/alert-dialog/alert-dialog-header.svelte +20 -0
  68. package/src/components/ui/alert-dialog/alert-dialog-media.svelte +20 -0
  69. package/src/components/ui/alert-dialog/alert-dialog-overlay.svelte +17 -0
  70. package/src/components/ui/alert-dialog/alert-dialog-portal.svelte +7 -0
  71. package/src/components/ui/alert-dialog/alert-dialog-title.svelte +17 -0
  72. package/src/components/ui/alert-dialog/alert-dialog-trigger.svelte +7 -0
  73. package/src/components/ui/alert-dialog/alert-dialog.svelte +7 -0
  74. package/src/components/ui/alert-dialog/index.ts +40 -0
  75. package/src/components/ui/button/button.svelte +81 -0
  76. package/src/components/ui/button/index.ts +17 -0
  77. package/src/components/ui/dialog/dialog-close.svelte +11 -0
  78. package/src/components/ui/dialog/dialog-content.svelte +48 -0
  79. package/src/components/ui/dialog/dialog-description.svelte +17 -0
  80. package/src/components/ui/dialog/dialog-footer.svelte +32 -0
  81. package/src/components/ui/dialog/dialog-header.svelte +20 -0
  82. package/src/components/ui/dialog/dialog-overlay.svelte +17 -0
  83. package/src/components/ui/dialog/dialog-portal.svelte +7 -0
  84. package/src/components/ui/dialog/dialog-title.svelte +17 -0
  85. package/src/components/ui/dialog/dialog-trigger.svelte +11 -0
  86. package/src/components/ui/dialog/dialog.svelte +7 -0
  87. package/src/components/ui/dialog/index.ts +34 -0
  88. package/src/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte +16 -0
  89. package/src/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +46 -0
  90. package/src/components/ui/dropdown-menu/dropdown-menu-content.svelte +31 -0
  91. package/src/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte +22 -0
  92. package/src/components/ui/dropdown-menu/dropdown-menu-group.svelte +7 -0
  93. package/src/components/ui/dropdown-menu/dropdown-menu-item.svelte +27 -0
  94. package/src/components/ui/dropdown-menu/dropdown-menu-label.svelte +24 -0
  95. package/src/components/ui/dropdown-menu/dropdown-menu-portal.svelte +7 -0
  96. package/src/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte +17 -0
  97. package/src/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte +36 -0
  98. package/src/components/ui/dropdown-menu/dropdown-menu-separator.svelte +17 -0
  99. package/src/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte +20 -0
  100. package/src/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte +31 -0
  101. package/src/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +29 -0
  102. package/src/components/ui/dropdown-menu/dropdown-menu-sub.svelte +7 -0
  103. package/src/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +7 -0
  104. package/src/components/ui/dropdown-menu/dropdown-menu.svelte +7 -0
  105. package/src/components/ui/dropdown-menu/index.ts +54 -0
  106. package/src/components/ui/empty/empty-content.svelte +23 -0
  107. package/src/components/ui/empty/empty-description.svelte +23 -0
  108. package/src/components/ui/empty/empty-header.svelte +20 -0
  109. package/src/components/ui/empty/empty-media.svelte +41 -0
  110. package/src/components/ui/empty/empty-title.svelte +20 -0
  111. package/src/components/ui/empty/empty.svelte +23 -0
  112. package/src/components/ui/empty/index.ts +22 -0
  113. package/src/components/ui/field/field-content.svelte +20 -0
  114. package/src/components/ui/field/field-description.svelte +25 -0
  115. package/src/components/ui/field/field-error.svelte +58 -0
  116. package/src/components/ui/field/field-group.svelte +23 -0
  117. package/src/components/ui/field/field-label.svelte +25 -0
  118. package/src/components/ui/field/field-legend.svelte +24 -0
  119. package/src/components/ui/field/field-separator.svelte +35 -0
  120. package/src/components/ui/field/field-set.svelte +20 -0
  121. package/src/components/ui/field/field-title.svelte +20 -0
  122. package/src/components/ui/field/field.svelte +47 -0
  123. package/src/components/ui/field/index.ts +33 -0
  124. package/src/components/ui/input/index.ts +7 -0
  125. package/src/components/ui/input/input.svelte +48 -0
  126. package/src/components/ui/label/index.ts +7 -0
  127. package/src/components/ui/label/label.svelte +20 -0
  128. package/src/components/ui/select/index.ts +40 -0
  129. package/src/components/ui/select/select-content.svelte +45 -0
  130. package/src/components/ui/select/select-group-heading.svelte +21 -0
  131. package/src/components/ui/select/select-group.svelte +17 -0
  132. package/src/components/ui/select/select-item.svelte +43 -0
  133. package/src/components/ui/select/select-label.svelte +20 -0
  134. package/src/components/ui/select/select-portal.svelte +7 -0
  135. package/src/components/ui/select/select-scroll-down-button.svelte +20 -0
  136. package/src/components/ui/select/select-scroll-up-button.svelte +20 -0
  137. package/src/components/ui/select/select-separator.svelte +18 -0
  138. package/src/components/ui/select/select-trigger.svelte +29 -0
  139. package/src/components/ui/select/select-value.svelte +7 -0
  140. package/src/components/ui/select/select.svelte +11 -0
  141. package/src/components/ui/separator/index.ts +7 -0
  142. package/src/components/ui/separator/separator.svelte +21 -0
  143. package/src/components/ui/sonner/index.ts +1 -0
  144. package/src/components/ui/sonner/sonner.svelte +40 -0
  145. package/src/components/ui/switch/index.ts +7 -0
  146. package/src/components/ui/switch/switch.svelte +34 -0
  147. package/src/components/ui/textarea/index.ts +7 -0
  148. package/src/components/ui/textarea/textarea.svelte +22 -0
  149. package/src/context.ts +34 -0
  150. package/src/dev/Demo.svelte +47 -0
  151. package/src/dev/main.ts +7 -0
  152. package/src/i18n/en.ts +645 -0
  153. package/src/i18n/index.ts +28 -0
  154. package/src/i18n/zh-CN.ts +610 -0
  155. package/src/index.ts +10 -0
  156. package/src/logs/LogPanel.svelte +103 -0
  157. package/src/logs/LogSettings.svelte +58 -0
  158. package/src/logs/index.ts +5 -0
  159. package/src/logs/logger.ts +96 -0
  160. package/src/markdown.ts +8 -0
  161. package/src/mcp/McpClientEditor.svelte +204 -0
  162. package/src/mcp/McpClientSettings.svelte +418 -0
  163. package/src/mcp/McpServerSettings.svelte +363 -0
  164. package/src/mcp/config.ts +26 -0
  165. package/src/mcp/index.ts +3 -0
  166. package/src/mcp/input.ts +74 -0
  167. package/src/notify.svelte.ts +43 -0
  168. package/src/ports/contract.ts +174 -0
  169. package/src/ports/fake-optional.ts +81 -0
  170. package/src/ports/fake.ts +488 -0
  171. package/src/ports/index.ts +7 -0
  172. package/src/ports/search.ts +56 -0
  173. package/src/ports/skill-sources.ts +45 -0
  174. package/src/ports/test-support/fake-fixture.ts +63 -0
  175. package/src/providers/EndpointKeysDialog.svelte +357 -0
  176. package/src/providers/EndpointModels.svelte +311 -0
  177. package/src/providers/EndpointPanel.svelte +266 -0
  178. package/src/providers/ModelPickerDialog.svelte +230 -0
  179. package/src/providers/ModelPricing.svelte +125 -0
  180. package/src/providers/ModelSelect.svelte +448 -0
  181. package/src/providers/ModelSettingsDialog.svelte +388 -0
  182. package/src/providers/ProviderDialogs.svelte +120 -0
  183. package/src/providers/ProviderList.svelte +159 -0
  184. package/src/providers/ProvidersSettings.svelte +491 -0
  185. package/src/providers/RequestConfigDialog.svelte +278 -0
  186. package/src/providers/SecretInput.svelte +284 -0
  187. package/src/providers/draft.ts +355 -0
  188. package/src/providers/edit-session.ts +170 -0
  189. package/src/providers/index.ts +5 -0
  190. package/src/providers/reasoning.ts +55 -0
  191. package/src/providers/revision.svelte.ts +6 -0
  192. package/src/providers/test-support/providers-fixture.ts +120 -0
  193. package/src/search/SearchAdvancedTools.svelte +182 -0
  194. package/src/search/SearchProviderCard.svelte +195 -0
  195. package/src/search/SearchSettings.svelte +500 -0
  196. package/src/search/index.ts +13 -0
  197. package/src/search/settings-flow.ts +67 -0
  198. package/src/session.ts +20 -0
  199. package/src/skills/SkillAdd.svelte +380 -0
  200. package/src/skills/SkillAuthoring.svelte +430 -0
  201. package/src/skills/SkillEditor.svelte +104 -0
  202. package/src/skills/SkillPreviewDialog.svelte +136 -0
  203. package/src/skills/SkillsSettings.svelte +147 -0
  204. package/src/skills/error.ts +32 -0
  205. package/src/skills/index.ts +4 -0
  206. package/src/skills/test-support/skills-fixture.ts +139 -0
  207. package/src/styles/kit.css +137 -0
  208. package/src/styles/tokens.css +22 -0
  209. package/src/tauri/index.ts +298 -0
  210. package/src/utils.ts +22 -0
  211. package/src/web/index.ts +441 -0
  212. package/README.md +0 -3
@@ -0,0 +1,491 @@
1
+ <script lang="ts">
2
+ // Provider settings (docs/33): list on the left, detail on the right, every field saves itself.
3
+ import { Button } from '#lib/components/ui/button/index.js'
4
+ import * as Empty from '#lib/components/ui/empty/index.js'
5
+ import * as Field from '#lib/components/ui/field/index.js'
6
+ import * as Select from '#lib/components/ui/select/index.js'
7
+ import { Switch } from '#lib/components/ui/switch/index.js'
8
+ import Settings2Icon from '@lucide/svelte/icons/settings-2'
9
+ import PlusIcon from '@lucide/svelte/icons/plus'
10
+ import { getKitContext } from '../context'
11
+ import { isKitClientError, type ProbeResult, type Provider, type ProviderPreset } from '../ports'
12
+ import { newEndpoint, savable, toDraft, validateModel, type ProviderDraft } from './draft'
13
+ import { createProviderEditSession, type ProviderEditSession } from './edit-session'
14
+ import EndpointPanel, { type CheckOutcome, type CheckState } from './EndpointPanel.svelte'
15
+ import ModelPickerDialog from './ModelPickerDialog.svelte'
16
+ import ProviderDialogs from './ProviderDialogs.svelte'
17
+ import ProviderList from './ProviderList.svelte'
18
+ import { providersRevision, settingsTarget } from './revision.svelte'
19
+
20
+ let {
21
+ onchanged,
22
+ presets = [],
23
+ usedBy,
24
+ }: {
25
+ onchanged?: () => void
26
+ presets?: ProviderPreset[]
27
+ /** Localized role names bound to a model, shown when its type changes (contract §6). */
28
+ usedBy?: (endpointId: string, modelId: string) => string[]
29
+ } = $props()
30
+ const { ports, i18n, notify } = getKitContext()
31
+ const { t } = i18n
32
+ const port = ports.providers
33
+
34
+ let providers = $state<Provider[]>([])
35
+ let draft = $state<ProviderDraft | null>(null)
36
+ /** Remounts the detail when another provider is loaded, resetting its local input state. */
37
+ let token = $state(0)
38
+ let active = $state(0)
39
+ let check = $state<{ slug: string; value: CheckState } | null>(null)
40
+ let probe = $state<{ slug: string; result: ProbeResult } | null>(null)
41
+ let pickerOpen = $state(false)
42
+ let failed = $state(false)
43
+ /** Last rejected model setting, named in the banner (docs/41 §4.3). */
44
+ let invalid = $state<{ model: string | null; message: string } | null>(null)
45
+ let enabledSaving = $state(false)
46
+ let addOpen = $state(false)
47
+ let newName = $state('')
48
+ let infoOpen = $state(false)
49
+ let editName = $state('')
50
+ let notesOpen = $state(false)
51
+ let editNotes = $state('')
52
+ let editSession: ProviderEditSession | null = null
53
+ let probeGeneration = 0
54
+
55
+ const endpoint = $derived(draft?.endpoints[active] ?? null)
56
+
57
+ function errorText(error: unknown): string {
58
+ return isKitClientError(error) ? t(`errors.${error.code}`) : String(error)
59
+ }
60
+ const MODEL_FIELDS: Record<string, true> = {
61
+ name: true,
62
+ groupName: true,
63
+ contextWindow: true,
64
+ maxOutput: true,
65
+ reasoning: true,
66
+ pricing: true,
67
+ }
68
+ /** A rejected model setting: the backend names field + reason; the model is `entity` or found by the local rules. */
69
+ function modelError(
70
+ error: unknown,
71
+ sent: ProviderDraft,
72
+ ): { model: string | null; message: string } | null {
73
+ if (!isKitClientError(error) || error.code !== 'bad_request') return null
74
+ const { field, reason, entity } = error.details
75
+ if (!field || !reason || !Object.hasOwn(MODEL_FIELDS, field)) return null
76
+ const models = sent.endpoints.flatMap((e) =>
77
+ e.models.map((model) => ({ model, protocol: e.protocol })),
78
+ )
79
+ const found =
80
+ models.find(({ model }) => model.id === entity) ??
81
+ models.find(({ model, protocol }) =>
82
+ validateModel(model, protocol).some(
83
+ (item) => item.field === field && item.reason === reason,
84
+ ),
85
+ )
86
+ const model = found ? (found.model.name ?? found.model.id) : null
87
+ return { model, message: t(`providers.error.${field}.${reason}`) }
88
+ }
89
+ function notifyChanged() {
90
+ providersRevision.value++
91
+ onchanged?.()
92
+ }
93
+ /** Key operations return the whole list; only the matching endpoint's stored record changes in the draft. */
94
+ function keysChanged(next: Provider[]) {
95
+ providers = next
96
+ const stored = next.find((p) => p.id === draft?.id)
97
+ for (const e of draft?.endpoints ?? []) {
98
+ const record = stored?.endpoints.find((s) => s.id === e.saved?.id)
99
+ if (record) e.saved = record
100
+ }
101
+ notifyChanged()
102
+ }
103
+
104
+ function load(next: ProviderDraft | null) {
105
+ editSession?.invalidate()
106
+ editSession = null
107
+ probeGeneration++
108
+ pickerOpen = false
109
+ check = null
110
+ probe = null
111
+ failed = false
112
+ invalid = null
113
+ draft = next
114
+ active = 0
115
+ token++
116
+ if (next && port)
117
+ editSession = createProviderEditSession(port, {
118
+ readDraft: () => {
119
+ if (!draft) throw new Error('Provider edit session has no draft')
120
+ return draft
121
+ },
122
+ writeDraft: (next) => {
123
+ draft = next
124
+ },
125
+ readProviders: () => providers,
126
+ onSaved: (next) => {
127
+ providers = next
128
+ notifyChanged()
129
+ },
130
+ })
131
+ }
132
+ async function select(p: Provider) {
133
+ if (p.id === draft?.id) return
134
+ if (
135
+ failed &&
136
+ !(await notify.confirm({
137
+ title: t('providers.discard_title'),
138
+ body: t('providers.discard_body'),
139
+ }))
140
+ )
141
+ return
142
+ load(toDraft(p))
143
+ }
144
+ async function refresh() {
145
+ if (!port) return
146
+ try {
147
+ providers = await port.list()
148
+ // Backend no-ops a preset already present and lets a same-named provider adopt it (docs/39 §3).
149
+ for (const preset of presets) {
150
+ if (providers.some((p) => p.presetId === preset.id)) continue
151
+ providers = await port.save({
152
+ name: preset.name,
153
+ presetId: preset.id,
154
+ enabled: false,
155
+ endpoints: [
156
+ { slug: 'ep-1', baseUrl: preset.baseUrl, protocol: preset.protocol, models: [] },
157
+ ],
158
+ })
159
+ }
160
+ const target = settingsTarget.value
161
+ const owner =
162
+ target && providers.find((p) => p.endpoints.some((e) => e.id === target.endpointId))
163
+ if (!owner) settingsTarget.value = null
164
+ load(owner ? toDraft(owner) : providers[0] ? toDraft(providers[0]) : null)
165
+ // EndpointPanel opens the target model's dialog when it mounts (and clears the target).
166
+ if (owner && target && draft)
167
+ active = Math.max(
168
+ 0,
169
+ draft.endpoints.findIndex((e) => e.saved?.id === target.endpointId),
170
+ )
171
+ } catch (error) {
172
+ notify.toast('error', errorText(error))
173
+ }
174
+ }
175
+ const keyUrl = $derived(
176
+ presets.find((preset) => preset.id === providers.find((p) => p.id === draft?.id)?.presetId)
177
+ ?.apiKeyUrl,
178
+ )
179
+ $effect(() => {
180
+ if (port) void refresh()
181
+ return () => {
182
+ editSession?.invalidate()
183
+ probeGeneration++
184
+ }
185
+ })
186
+
187
+ /** Presentation only; the ordinary TS session owns the queue and its save barrier. */
188
+ async function commit() {
189
+ const session = editSession
190
+ if (!session || !draft) return
191
+ const sent = $state.snapshot(savable(draft)) as ProviderDraft
192
+ try {
193
+ await session.commit()
194
+ if (editSession !== session) return
195
+ failed = false
196
+ invalid = null
197
+ } catch (error) {
198
+ if (editSession !== session || (isKitClientError(error) && error.code === 'cancelled')) return
199
+ failed = true
200
+ invalid = modelError(error, sent)
201
+ if (!invalid) notify.toast('error', errorText(error))
202
+ }
203
+ }
204
+
205
+ async function setEnabled(enabled: boolean) {
206
+ const id = draft?.id
207
+ if (!draft || !id || !port || enabledSaving) return
208
+ const previous = draft.enabled
209
+ draft.enabled = enabled
210
+ enabledSaving = true
211
+ try {
212
+ providers = await port.setEnabled(id, enabled)
213
+ notifyChanged()
214
+ } catch (error) {
215
+ if (draft?.id === id) draft.enabled = previous
216
+ notify.toast('error', errorText(error))
217
+ } finally {
218
+ enabledSaving = false
219
+ }
220
+ }
221
+
222
+ async function create() {
223
+ const name = newName.trim()
224
+ if (!name) return
225
+ addOpen = false
226
+ newName = ''
227
+ load({ id: null, name, enabled: true, notes: '', endpoints: [newEndpoint([])] })
228
+ await commit()
229
+ }
230
+ function rename() {
231
+ const name = editName.trim()
232
+ infoOpen = false
233
+ if (!draft || !name || name === draft.name) return
234
+ draft.name = name
235
+ void commit()
236
+ }
237
+ /** Row menu actions act on that row, so select it first (honours the unsaved-changes prompt). */
238
+ async function rowAction(p: Provider, action: 'edit' | 'notes' | 'delete') {
239
+ await select(p)
240
+ if (draft?.id !== p.id) return
241
+ if (action === 'delete') return remove()
242
+ editName = draft.name
243
+ editNotes = draft.notes
244
+ if (action === 'edit') infoOpen = true
245
+ else notesOpen = true
246
+ }
247
+ function saveNotes() {
248
+ notesOpen = false
249
+ if (!draft || editNotes === draft.notes) return
250
+ draft.notes = editNotes
251
+ void commit()
252
+ }
253
+ async function remove() {
254
+ const current = draft
255
+ const session = editSession
256
+ if (!current || !port) return
257
+ infoOpen = false
258
+ if (current.id === null) return load(providers[0] ? toDraft(providers[0]) : null)
259
+ const body = t('providers.delete_confirm', { name: current.name })
260
+ if (!(await notify.confirm({ title: t('providers.delete'), body, danger: true }))) return
261
+ if (editSession !== session) return
262
+ session?.invalidate()
263
+ probeGeneration++
264
+ pickerOpen = false
265
+ try {
266
+ const next = await port.remove(current.id)
267
+ if (editSession !== session) return
268
+ providers = next
269
+ load(providers[0] ? toDraft(providers[0]) : null)
270
+ notifyChanged()
271
+ } catch (error) {
272
+ if (editSession !== session) return
273
+ load(current)
274
+ notify.toast('error', errorText(error))
275
+ }
276
+ }
277
+ function addEndpoint() {
278
+ if (!draft) return
279
+ draft.endpoints.push(newEndpoint(draft.endpoints))
280
+ active = draft.endpoints.length - 1
281
+ // Stored right away so its keys can be added before it has an address (docs/34 §2).
282
+ void commit()
283
+ }
284
+ async function removeEndpoint() {
285
+ if (!draft || !endpoint) return
286
+ const index = active
287
+ if (
288
+ endpoint.saved &&
289
+ !(await notify.confirm({
290
+ title: t('providers.remove_endpoint'),
291
+ body: t('providers.remove_endpoint_confirm'),
292
+ danger: true,
293
+ }))
294
+ )
295
+ return
296
+ const wasSaved = draft.endpoints[index]?.saved !== null
297
+ draft.endpoints.splice(index, 1)
298
+ active = 0
299
+ if (wasSaved) void commit()
300
+ }
301
+
302
+ function outcome(result: ProbeResult): CheckOutcome {
303
+ const e = result.error
304
+ if (!e) return { state: 'ok', count: result.models.length, suggested: result.suggestedProtocol }
305
+ const message = [
306
+ t(`providers.probe.${e.kind}`, { status: e.status ?? '' }),
307
+ e.hint ? t(e.hint) : '',
308
+ e.message ?? '',
309
+ ]
310
+ .filter(Boolean)
311
+ .join('\n')
312
+ return { state: 'error', message }
313
+ }
314
+ async function runProbe(index: number, openPicker: boolean) {
315
+ const target = draft?.endpoints[index]
316
+ const session = editSession
317
+ if (!target || !session) return
318
+ const generation = ++probeGeneration
319
+ const slug = target.slug
320
+ check = { slug, value: { state: 'running' } }
321
+ try {
322
+ const batch = await session.probe(slug)
323
+ if (session !== editSession || generation !== probeGeneration) return
324
+ failed = false
325
+ invalid = null
326
+ const first = batch.results[0]
327
+ check = {
328
+ slug,
329
+ value:
330
+ first?.keyId === null
331
+ ? outcome(first.result)
332
+ : {
333
+ state: 'keys',
334
+ results: batch.results.flatMap(({ keyId, result }) =>
335
+ keyId === null ? [] : [{ keyId, value: outcome(result) }],
336
+ ),
337
+ },
338
+ }
339
+ if (openPicker && batch.usable) {
340
+ probe = { slug, result: batch.usable }
341
+ pickerOpen = true
342
+ }
343
+ } catch (error) {
344
+ if (session !== editSession || generation !== probeGeneration) return
345
+ check =
346
+ isKitClientError(error) && error.code === 'cancelled'
347
+ ? null
348
+ : { slug, value: { state: 'error', message: errorText(error) } }
349
+ }
350
+ }
351
+ const probed = $derived(
352
+ probe ? (draft?.endpoints.find((e) => e.slug === probe?.slug) ?? null) : null,
353
+ )
354
+ </script>
355
+
356
+ {#if port}
357
+ <div class="flex min-h-0 min-w-0 flex-col gap-4 sm:flex-row">
358
+ <ProviderList
359
+ {providers}
360
+ selectedId={draft?.id ?? null}
361
+ onselect={(provider) => void select(provider)}
362
+ onaction={(provider, action) => void rowAction(provider, action)}
363
+ onadd={() => (addOpen = true)}
364
+ />
365
+
366
+ <section class="flex min-w-0 flex-1 flex-col gap-5">
367
+ {#if draft}
368
+ <header class="flex min-h-8 min-w-0 items-center gap-1.5">
369
+ <h2 class="min-w-0 truncate text-[15px] font-semibold">{draft.name}</h2>
370
+ <Button
371
+ variant="ghost"
372
+ size="icon-xs"
373
+ class="text-muted-foreground"
374
+ title={t('providers.settings')}
375
+ aria-label={t('providers.settings')}
376
+ onclick={() => {
377
+ editName = draft?.name ?? ''
378
+ infoOpen = true
379
+ }}><Settings2Icon /></Button
380
+ >
381
+ <Switch
382
+ id="kit-provider-enabled"
383
+ class="ml-auto"
384
+ aria-label={t('providers.enabled')}
385
+ checked={draft.enabled}
386
+ disabled={enabledSaving || draft.id === null}
387
+ onCheckedChange={(enabled) => void setEnabled(enabled)}
388
+ />
389
+ </header>
390
+ {#if failed}
391
+ <p role="alert" class="text-destructive flex items-center gap-2 text-kit-sm">
392
+ {invalid
393
+ ? invalid.model
394
+ ? t('providers.save_model_invalid', {
395
+ model: invalid.model,
396
+ error: invalid.message,
397
+ })
398
+ : invalid.message
399
+ : t('providers.save_failed')}
400
+ <Button variant="link" size="sm" class="h-auto p-0" onclick={() => void commit()}
401
+ >{t('providers.retry')}</Button
402
+ >
403
+ </p>
404
+ {/if}
405
+ {#if draft.endpoints.length > 1}
406
+ <Field.Field orientation="horizontal" class="gap-2">
407
+ <Field.Label for="kit-provider-endpoint" class="shrink-0"
408
+ >{t('providers.endpoints')}</Field.Label
409
+ >
410
+ <Select.Root
411
+ type="single"
412
+ value={String(active)}
413
+ onValueChange={(value) => (active = Number(value))}
414
+ >
415
+ <Select.Trigger id="kit-provider-endpoint" class="min-w-0 flex-1">
416
+ <span class="truncate">{endpoint?.baseUrl || t('providers.endpoint_new')}</span>
417
+ </Select.Trigger>
418
+ <Select.Content>
419
+ <Select.Group>
420
+ {#each draft.endpoints as e, index (e.slug)}
421
+ <Select.Item
422
+ value={String(index)}
423
+ label={e.baseUrl || t('providers.endpoint_new')}
424
+ />
425
+ {/each}
426
+ </Select.Group>
427
+ </Select.Content>
428
+ </Select.Root>
429
+ </Field.Field>
430
+ {/if}
431
+ {#if endpoint}
432
+ {#key `${token}:${endpoint.slug}`}
433
+ <EndpointPanel
434
+ bind:endpoint={
435
+ () => endpoint,
436
+ (next) => {
437
+ if (draft) draft.endpoints[active] = next
438
+ }
439
+ }
440
+ check={check?.slug === endpoint.slug ? check.value : null}
441
+ onaddendpoint={draft.endpoints.length < 20 ? addEndpoint : null}
442
+ onremove={() => void removeEndpoint()}
443
+ oncommit={() => void commit()}
444
+ onkeys={keysChanged}
445
+ oncheck={() => void runProbe(active, false)}
446
+ onsync={() => void runProbe(active, true)}
447
+ {keyUrl}
448
+ {usedBy}
449
+ />
450
+ {/key}
451
+ {:else}
452
+ <Button variant="outline" class="self-start" onclick={addEndpoint}>
453
+ <PlusIcon />{t('providers.add_endpoint')}
454
+ </Button>
455
+ {/if}
456
+ {:else}
457
+ <Empty.Root>
458
+ <Empty.Header>
459
+ <Empty.Title>{t('providers.empty')}</Empty.Title>
460
+ <Empty.Description>{t('providers.empty_hint')}</Empty.Description>
461
+ </Empty.Header>
462
+ </Empty.Root>
463
+ {/if}
464
+ </section>
465
+ </div>
466
+
467
+ <ModelPickerDialog
468
+ bind:open={pickerOpen}
469
+ result={probe?.result ?? null}
470
+ selected={probed?.models ?? []}
471
+ onchange={(models) => {
472
+ if (!probed) return
473
+ probed.models = models
474
+ void commit()
475
+ }}
476
+ />
477
+ <ProviderDialogs
478
+ bind:addOpen
479
+ bind:infoOpen
480
+ bind:notesOpen
481
+ bind:newName
482
+ bind:editName
483
+ bind:editNotes
484
+ {draft}
485
+ {providers}
486
+ {create}
487
+ {rename}
488
+ {saveNotes}
489
+ {remove}
490
+ />
491
+ {/if}