@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,388 @@
1
+ <script lang="ts">
2
+ // Per-model settings (docs/41 §3–§4). Edits a copy; nothing changes until Save.
3
+ import { untrack } from 'svelte'
4
+ import ChevronRightIcon from '@lucide/svelte/icons/chevron-right'
5
+ import ModelPricing from './ModelPricing.svelte'
6
+ import { Button } from '#lib/components/ui/button/index.js'
7
+ import * as Dialog from '#lib/components/ui/dialog/index.js'
8
+ import * as Field from '#lib/components/ui/field/index.js'
9
+ import { Input } from '#lib/components/ui/input/index.js'
10
+ import * as Select from '#lib/components/ui/select/index.js'
11
+ import CopyInput from '../CopyInput.svelte'
12
+ import { getKitContext } from '../context'
13
+ import type { Model, ModelType, Protocol, ReasoningDialect, ReasoningEffort } from '../ports'
14
+ import { cn } from '../utils'
15
+ import {
16
+ idPrefix,
17
+ purposeOf,
18
+ validateModel,
19
+ withPurpose,
20
+ withType,
21
+ type ModelField,
22
+ type Purpose,
23
+ } from './draft'
24
+ import {
25
+ REASONING_DIALECTS,
26
+ REASONING_EFFORTS,
27
+ allowsAuto,
28
+ dialectFits,
29
+ emptyReasoning,
30
+ } from './reasoning'
31
+
32
+ let {
33
+ open = $bindable(false),
34
+ model,
35
+ protocol,
36
+ usedBy = [],
37
+ onsave,
38
+ }: {
39
+ open: boolean
40
+ model: Model | null
41
+ protocol: Protocol
42
+ /** Localized role names bound to this model (contract §6). */
43
+ usedBy?: string[]
44
+ onsave: (model: Model) => void
45
+ } = $props()
46
+
47
+ const MAIN_TYPES = ['chat', 'image', 'embedding', 'rerank'] as const
48
+ const OTHER_TYPES: ModelType[] = ['video', 'tts', 'stt', 'unknown']
49
+ // Presets mix decimal and binary sizes, so labels are explicit.
50
+ const CONTEXT_PRESETS: [string, number][] = [
51
+ ['128K', 128000],
52
+ ['200K', 200000],
53
+ ['256K', 262144],
54
+ ['400K', 400000],
55
+ ['512K', 524288],
56
+ ['1M', 1000000],
57
+ ]
58
+ const OUTPUT_PRESETS: [string, number][] = [
59
+ ['16K', 16384],
60
+ ['32K', 32768],
61
+ ['64K', 65536],
62
+ ['128K', 131072],
63
+ ]
64
+ const PURPOSES: Purpose[] = ['chat', 'image', 'edit']
65
+ const CHIP = 'aria-pressed:border-primary aria-pressed:bg-primary/10 aria-pressed:text-primary'
66
+
67
+ const { t } = getKitContext().i18n
68
+ let edit = $state<Model | null>(null)
69
+ let more = $state(true)
70
+ $effect.pre(() => {
71
+ if (!open) return
72
+ edit = untrack(() => (model ? $state.snapshot(model) : null))
73
+ more = true
74
+ })
75
+ const dialects = $derived(REASONING_DIALECTS.filter((dialect) => dialectFits(dialect, protocol)))
76
+ const errors = $derived(edit ? validateModel(edit, protocol) : [])
77
+ const purpose = $derived(edit && purposeOf(edit))
78
+ const count = (value: number) => (Number.isNaN(value) ? null : value)
79
+
80
+ function errorFor(field: ModelField) {
81
+ return errors
82
+ .filter((item) => item.field === field)
83
+ .map((item) => ({ message: t(`providers.error.${field}.${item.reason}`) }))
84
+ }
85
+ function toggleEffort(effort: ReasoningEffort) {
86
+ const config = edit?.reasoning
87
+ if (!edit || !config) return
88
+ const efforts = config.efforts.includes(effort)
89
+ ? config.efforts.filter((item) => item !== effort)
90
+ : [...config.efforts, effort]
91
+ edit.reasoning = { ...config, efforts }
92
+ }
93
+ function setBudget(value: number) {
94
+ if (!edit?.reasoning) return
95
+ const rest = { ...edit.reasoning }
96
+ delete rest.budgetTokens
97
+ edit.reasoning = Number.isNaN(value) ? rest : { ...rest, budgetTokens: value }
98
+ }
99
+ /** Tri-state: not probed → supported → unsupported → not probed. */
100
+ function cycleTools() {
101
+ if (edit)
102
+ edit.supportsTools = edit.supportsTools === null ? true : edit.supportsTools ? false : null
103
+ }
104
+ function save() {
105
+ if (!edit || errors.length > 0) return
106
+ const name = edit.name?.trim() || null
107
+ const groupName = edit.groupName?.trim() || null
108
+ onsave({ ...$state.snapshot(edit), name, groupName })
109
+ open = false
110
+ }
111
+ </script>
112
+
113
+ <Dialog.Root bind:open>
114
+ <Dialog.Content class="flex max-h-[85dvh] min-w-0 flex-col sm:max-w-lg">
115
+ <Dialog.Header class="min-w-0 text-left">
116
+ <Dialog.Title>{t('providers.edit_model')}</Dialog.Title>
117
+ </Dialog.Header>
118
+ {#if edit}
119
+ <Field.Group class="min-h-0 overflow-y-auto pr-1">
120
+ <Field.Field>
121
+ <Field.Label for="kit-model-id">{t('providers.model_id')}</Field.Label>
122
+ <CopyInput id="kit-model-id" class="font-mono" readonly value={edit.id} />
123
+ </Field.Field>
124
+ <Field.Field>
125
+ <Field.Label for="kit-model-name">{t('providers.model_name')}</Field.Label>
126
+ <Input
127
+ id="kit-model-name"
128
+ placeholder={edit.id}
129
+ autocomplete="off"
130
+ aria-invalid={errorFor('name').length > 0}
131
+ bind:value={() => edit?.name ?? '', (value) => edit && (edit.name = value)}
132
+ />
133
+ <Field.Error errors={errorFor('name')} />
134
+ </Field.Field>
135
+ <Field.Field>
136
+ <Field.Label for="kit-model-group">{t('providers.group_name')}</Field.Label>
137
+ <Input
138
+ id="kit-model-group"
139
+ placeholder={idPrefix(edit.id)}
140
+ autocomplete="off"
141
+ aria-invalid={errorFor('groupName').length > 0}
142
+ bind:value={() => edit?.groupName ?? '', (value) => edit && (edit.groupName = value)}
143
+ />
144
+ <Field.Error errors={errorFor('groupName')} />
145
+ </Field.Field>
146
+ {#if purpose}
147
+ <Field.Set>
148
+ <Field.Legend variant="label">{t('providers.purpose')}</Field.Legend>
149
+ <div class="flex flex-wrap gap-4">
150
+ {#each PURPOSES as item (item)}
151
+ <label class="flex items-center gap-1.5 text-kit-sm"
152
+ ><input
153
+ type="radio"
154
+ name="kit-model-purpose"
155
+ checked={purpose === item}
156
+ onchange={() => edit && (edit = withPurpose(edit, item))}
157
+ />{t(`providers.purpose.${item}`)}</label
158
+ >
159
+ {/each}
160
+ </div>
161
+ </Field.Set>
162
+ {/if}
163
+ <Button
164
+ variant="ghost"
165
+ size="sm"
166
+ class="justify-start self-start px-1"
167
+ aria-expanded={more}
168
+ onclick={() => (more = !more)}
169
+ ><ChevronRightIcon class={cn('transition-transform', more && 'rotate-90')} />{t(
170
+ 'providers.more_settings',
171
+ )}</Button
172
+ >
173
+ {#if more}
174
+ <Field.Set>
175
+ <Field.Legend variant="label">{t('providers.model_type')}</Field.Legend>
176
+ <div class="flex flex-wrap gap-1.5">
177
+ {#each MAIN_TYPES as type (type)}
178
+ <Button
179
+ variant="outline"
180
+ size="sm"
181
+ class={CHIP}
182
+ aria-pressed={edit.modelType === type}
183
+ onclick={() => edit && (edit = withType(edit, type))}
184
+ >{t(`providers.type.${type}`)}</Button
185
+ >
186
+ {/each}
187
+ <Select.Root
188
+ type="single"
189
+ value={OTHER_TYPES.includes(edit.modelType) ? edit.modelType : ''}
190
+ onValueChange={(value) =>
191
+ edit && value && (edit = withType(edit, value as ModelType))}
192
+ >
193
+ <Select.Trigger
194
+ size="sm"
195
+ class={cn(
196
+ OTHER_TYPES.includes(edit.modelType) &&
197
+ 'border-primary bg-primary/10 text-primary',
198
+ )}
199
+ aria-label={t('providers.type_other')}
200
+ >{OTHER_TYPES.includes(edit.modelType)
201
+ ? t(`providers.type.${edit.modelType}`)
202
+ : t('providers.type_other')}</Select.Trigger
203
+ >
204
+ <Select.Content>
205
+ <Select.Group>
206
+ {#each OTHER_TYPES as type (type)}<Select.Item
207
+ value={type}
208
+ label={t(`providers.type.${type}`)}
209
+ />{/each}
210
+ </Select.Group>
211
+ </Select.Content>
212
+ </Select.Root>
213
+ </div>
214
+ {#if usedBy.length > 0 && edit.modelType !== model?.modelType}
215
+ <Field.Description class="text-warning text-kit-xs" role="status"
216
+ >{t('providers.type_in_use', {
217
+ roles: usedBy.join(t('providers.list_separator')),
218
+ })}</Field.Description
219
+ >
220
+ {/if}
221
+ </Field.Set>
222
+ {#if edit.modelType === 'chat'}
223
+ <Field.Set>
224
+ <Field.Legend variant="label">{t('providers.capabilities')}</Field.Legend>
225
+ <div class="flex flex-wrap gap-1.5">
226
+ <Button
227
+ variant="outline"
228
+ size="sm"
229
+ class={CHIP}
230
+ aria-pressed={edit.reasoning !== null}
231
+ onclick={() =>
232
+ edit &&
233
+ (edit.reasoning = edit.reasoning
234
+ ? null
235
+ : emptyReasoning(dialects[0] ?? 'openai'))}
236
+ >{t('providers.cap_reasoning')}</Button
237
+ >
238
+ <Button
239
+ variant="outline"
240
+ size="sm"
241
+ class={cn(
242
+ CHIP,
243
+ edit.supportsTools === false && 'text-muted-foreground line-through',
244
+ )}
245
+ aria-pressed={edit.supportsTools === true}
246
+ title={t(`providers.tools_state.${edit.supportsTools ?? 'unknown'}`)}
247
+ onclick={cycleTools}
248
+ >{t('providers.cap_tools')}{#if edit.supportsTools === null}<span
249
+ class="text-muted-foreground text-kit-xs"
250
+ >{t('providers.tools_state.unknown')}</span
251
+ >{/if}</Button
252
+ >
253
+ {#if protocol !== 'openai-chat'}
254
+ <Button
255
+ variant="outline"
256
+ size="sm"
257
+ class={CHIP}
258
+ aria-pressed={edit.nativeWebSearch}
259
+ title={t('providers.nativeWebSearch')}
260
+ onclick={() => edit && (edit.nativeWebSearch = !edit.nativeWebSearch)}
261
+ >{t('providers.cap_web')}</Button
262
+ >
263
+ {/if}
264
+ </div>
265
+ </Field.Set>
266
+ {#if edit.reasoning}
267
+ {@render reasoningControls(edit.reasoning.dialect)}
268
+ {/if}
269
+ <Field.Set>
270
+ <Field.Legend variant="label">{t('providers.input_modalities')}</Field.Legend>
271
+ <div class="flex flex-wrap gap-1.5">
272
+ <Button
273
+ variant="outline"
274
+ size="sm"
275
+ class={CHIP}
276
+ aria-pressed={edit.canVision}
277
+ onclick={() => edit && (edit.canVision = !edit.canVision)}
278
+ >{t('providers.cap_vision')}</Button
279
+ >
280
+ </div>
281
+ </Field.Set>
282
+ {/if}
283
+ {@render tokens('contextWindow', CONTEXT_PRESETS, 32000)}
284
+ {@render tokens('maxOutput', OUTPUT_PRESETS, 4096)}
285
+ {#if edit.modelType === 'chat'}
286
+ <ModelPricing {edit} {errorFor} />
287
+ {/if}
288
+ {/if}
289
+ </Field.Group>
290
+ {/if}
291
+ <Dialog.Footer class="items-center">
292
+ <p class="text-muted-foreground mr-auto text-kit-xs">{t('providers.applies_next')}</p>
293
+ <Button variant="outline" onclick={() => (open = false)}>{t('common.cancel')}</Button>
294
+ <Button disabled={errors.length > 0} onclick={save}>{t('providers.save')}</Button>
295
+ </Dialog.Footer>
296
+ </Dialog.Content>
297
+ </Dialog.Root>
298
+
299
+ {#snippet reasoningControls(dialect: ReasoningDialect)}
300
+ <Field.Field>
301
+ <Field.Label for="kit-model-dialect">{t('providers.reasoning_dialect')}</Field.Label>
302
+ <Select.Root
303
+ type="single"
304
+ value={dialect}
305
+ onValueChange={(value) =>
306
+ edit && (edit.reasoning = emptyReasoning(value as ReasoningDialect))}
307
+ >
308
+ <Select.Trigger id="kit-model-dialect" class="w-full"
309
+ >{t(`providers.dialect.${dialect}`)}</Select.Trigger
310
+ >
311
+ <Select.Content>
312
+ <Select.Group>
313
+ {#each dialects as item (item)}<Select.Item
314
+ value={item}
315
+ label={t(`providers.dialect.${item}`)}
316
+ />{/each}
317
+ </Select.Group>
318
+ </Select.Content>
319
+ </Select.Root>
320
+ </Field.Field>
321
+ <Field.Set>
322
+ <Field.Legend variant="label">{t('providers.reasoning_efforts')}</Field.Legend>
323
+ <div class="flex flex-wrap gap-3">
324
+ {#each REASONING_EFFORTS.filter((effort) => effort !== 'auto' || allowsAuto(dialect)) as effort (effort)}
325
+ <label class="flex items-center gap-1 text-kit-sm"
326
+ ><input
327
+ type="checkbox"
328
+ checked={edit?.reasoning?.efforts.includes(effort)}
329
+ onchange={() => toggleEffort(effort)}
330
+ />{t(`chat.effort.${effort}`)}</label
331
+ >
332
+ {/each}
333
+ </div>
334
+ {#if dialect !== 'anthropic-budget' && dialect !== 'qwen'}<Field.Error
335
+ errors={errorFor('reasoning')}
336
+ />{/if}
337
+ </Field.Set>
338
+ {#if dialect === 'anthropic-budget' || dialect === 'qwen'}
339
+ <Field.Field>
340
+ <Field.Label for="kit-model-budget">{t('providers.reasoning_budget')}</Field.Label>
341
+ <Input
342
+ id="kit-model-budget"
343
+ type="number"
344
+ min={dialect === 'qwen' ? '1' : '1024'}
345
+ step="1"
346
+ aria-invalid={errorFor('reasoning').length > 0}
347
+ value={edit?.reasoning?.budgetTokens ?? ''}
348
+ oninput={(event) => setBudget(event.currentTarget.valueAsNumber)}
349
+ />
350
+ <Field.Error errors={errorFor('reasoning')} />
351
+ </Field.Field>
352
+ {/if}
353
+ {/snippet}
354
+
355
+ {#snippet tokens(
356
+ field: 'contextWindow' | 'maxOutput',
357
+ presets: [string, number][],
358
+ fallback: number,
359
+ )}
360
+ {@const value = edit?.[field] ?? null}
361
+ <Field.Field>
362
+ <Field.Label for="kit-model-{field}"
363
+ >{t(`providers.${field === 'contextWindow' ? 'context_window' : 'max_output'}`)}</Field.Label
364
+ >
365
+ <div class="flex flex-wrap gap-1.5">
366
+ {#each presets as [label, preset] (preset)}
367
+ <Button
368
+ variant="outline"
369
+ size="sm"
370
+ class={CHIP}
371
+ aria-pressed={value === preset}
372
+ onclick={() => edit && (edit[field] = preset)}>{label}</Button
373
+ >
374
+ {/each}
375
+ </div>
376
+ <Input
377
+ id="kit-model-{field}"
378
+ type="number"
379
+ min="1"
380
+ step="1"
381
+ placeholder={String(fallback)}
382
+ aria-invalid={errorFor(field).length > 0}
383
+ value={value ?? ''}
384
+ oninput={(event) => edit && (edit[field] = count(event.currentTarget.valueAsNumber))}
385
+ />
386
+ <Field.Error errors={errorFor(field)} />
387
+ </Field.Field>
388
+ {/snippet}
@@ -0,0 +1,120 @@
1
+ <script lang="ts">
2
+ import * as Dialog from '#lib/components/ui/dialog/index.js'
3
+ import * as Field from '#lib/components/ui/field/index.js'
4
+ import { Input } from '#lib/components/ui/input/index.js'
5
+ import { Textarea } from '#lib/components/ui/textarea/index.js'
6
+ import { Button } from '#lib/components/ui/button/index.js'
7
+ import type { Provider } from '../ports'
8
+ import type { ProviderDraft } from './draft'
9
+ import { getKitContext } from '../context'
10
+
11
+ let {
12
+ addOpen = $bindable(false),
13
+ infoOpen = $bindable(false),
14
+ notesOpen = $bindable(false),
15
+ newName = $bindable(''),
16
+ editName = $bindable(''),
17
+ editNotes = $bindable(''),
18
+ draft,
19
+ providers,
20
+ create,
21
+ rename,
22
+ saveNotes,
23
+ remove,
24
+ }: {
25
+ addOpen?: boolean
26
+ infoOpen?: boolean
27
+ notesOpen?: boolean
28
+ newName?: string
29
+ editName?: string
30
+ editNotes?: string
31
+ draft: ProviderDraft | null
32
+ providers: Provider[]
33
+ create: () => void | Promise<void>
34
+ rename: () => void
35
+ saveNotes: () => void
36
+ remove: () => void | Promise<void>
37
+ } = $props()
38
+ const { i18n } = getKitContext()
39
+ const { t } = i18n
40
+ </script>
41
+
42
+ <Dialog.Root bind:open={addOpen}>
43
+ <Dialog.Content class="sm:max-w-md">
44
+ <Dialog.Header><Dialog.Title>{t('providers.add')}</Dialog.Title></Dialog.Header>
45
+ <form
46
+ class="contents"
47
+ onsubmit={(event) => {
48
+ event.preventDefault()
49
+ void create()
50
+ }}
51
+ >
52
+ <Field.Field>
53
+ <Field.Label for="kit-provider-new">{t('providers.name')}</Field.Label>
54
+ <Input id="kit-provider-new" maxlength={80} bind:value={newName} />
55
+ </Field.Field>
56
+ <Dialog.Footer>
57
+ <Button type="button" variant="outline" onclick={() => (addOpen = false)}
58
+ >{t('common.cancel')}</Button
59
+ >
60
+ <Button type="submit" disabled={!newName.trim()}>{t('common.confirm')}</Button>
61
+ </Dialog.Footer>
62
+ </form>
63
+ </Dialog.Content>
64
+ </Dialog.Root>
65
+
66
+ <Dialog.Root bind:open={infoOpen}>
67
+ <Dialog.Content class="sm:max-w-md">
68
+ <Dialog.Header><Dialog.Title>{t('providers.settings')}</Dialog.Title></Dialog.Header>
69
+ <form
70
+ class="contents"
71
+ onsubmit={(event) => {
72
+ event.preventDefault()
73
+ rename()
74
+ }}
75
+ >
76
+ <Field.Field>
77
+ <Field.Label for="kit-provider-name">{t('providers.name')}</Field.Label>
78
+ <Input id="kit-provider-name" maxlength={80} bind:value={editName} />
79
+ </Field.Field>
80
+ <Dialog.Footer class="sm:justify-between">
81
+ {#if !providers.find((p) => p.id === draft?.id)?.presetId}
82
+ <Button
83
+ type="button"
84
+ variant="ghost"
85
+ class="text-destructive"
86
+ onclick={() => void remove()}>{t('providers.delete')}</Button
87
+ >
88
+ {:else}<span></span>{/if}
89
+ <Button type="submit" disabled={!editName.trim()}>{t('providers.save')}</Button>
90
+ </Dialog.Footer>
91
+ </form>
92
+ </Dialog.Content>
93
+ </Dialog.Root>
94
+
95
+ <Dialog.Root bind:open={notesOpen}>
96
+ <Dialog.Content class="sm:max-w-lg">
97
+ <Dialog.Header><Dialog.Title>{t('providers.notes')}</Dialog.Title></Dialog.Header>
98
+ <form
99
+ class="contents"
100
+ onsubmit={(event) => {
101
+ event.preventDefault()
102
+ saveNotes()
103
+ }}
104
+ >
105
+ <Textarea
106
+ class="min-h-64 font-mono text-kit-sm"
107
+ aria-label={t('providers.notes')}
108
+ placeholder={t('providers.notes_placeholder')}
109
+ maxlength={10000}
110
+ bind:value={editNotes}
111
+ />
112
+ <Dialog.Footer>
113
+ <Button type="button" variant="outline" onclick={() => (notesOpen = false)}
114
+ >{t('common.cancel')}</Button
115
+ >
116
+ <Button type="submit">{t('providers.save')}</Button>
117
+ </Dialog.Footer>
118
+ </form>
119
+ </Dialog.Content>
120
+ </Dialog.Root>
@@ -0,0 +1,159 @@
1
+ <script lang="ts">
2
+ import EllipsisIcon from '@lucide/svelte/icons/ellipsis-vertical'
3
+ import PlusIcon from '@lucide/svelte/icons/plus'
4
+ import SearchIcon from '@lucide/svelte/icons/search'
5
+ import SquarePenIcon from '@lucide/svelte/icons/square-pen'
6
+ import SlidersHorizontalIcon from '@lucide/svelte/icons/sliders-horizontal'
7
+ import Trash2Icon from '@lucide/svelte/icons/trash-2'
8
+ import UserPenIcon from '@lucide/svelte/icons/user-pen'
9
+ import { Button } from '#lib/components/ui/button/index.js'
10
+ import * as DropdownMenu from '#lib/components/ui/dropdown-menu/index.js'
11
+ import { Input } from '#lib/components/ui/input/index.js'
12
+ import * as Select from '#lib/components/ui/select/index.js'
13
+ import { getKitContext } from '../context'
14
+ import type { Provider } from '../ports'
15
+ import { cn } from '../utils'
16
+
17
+ let {
18
+ providers,
19
+ selectedId,
20
+ onselect,
21
+ onaction,
22
+ onadd,
23
+ }: {
24
+ providers: Provider[]
25
+ selectedId: string | null
26
+ onselect: (provider: Provider) => void
27
+ onaction: (provider: Provider, action: 'edit' | 'notes' | 'delete') => void
28
+ onadd: () => void
29
+ } = $props()
30
+ const { i18n } = getKitContext()
31
+ const { t } = i18n
32
+ const FILTERS = ['all', 'enabled', 'disabled'] as const
33
+ let query = $state('')
34
+ let filter = $state<(typeof FILTERS)[number]>('all')
35
+ let menuFor = $state<string | null>(null)
36
+ const visible = $derived(
37
+ providers.filter(
38
+ (p) =>
39
+ p.name.toLocaleLowerCase().includes(query.trim().toLocaleLowerCase()) &&
40
+ (filter === 'all' || (filter === 'enabled') === p.enabled),
41
+ ),
42
+ )
43
+ function status(p: Provider): string | null {
44
+ if (!p.enabled) return null
45
+ return p.endpoints.length === 0 ||
46
+ p.endpoints.some((e) => !e.keys.some((k) => k.enabled && k.readable))
47
+ ? 'bg-warning'
48
+ : 'bg-success'
49
+ }
50
+ </script>
51
+
52
+ <nav class="flex w-full shrink-0 flex-col gap-1 sm:w-60" aria-label={t('providers.title')}>
53
+ <div class="mb-1 flex items-center gap-1.5">
54
+ <div class="relative min-w-0 flex-1">
55
+ <SearchIcon
56
+ class="text-muted-foreground pointer-events-none absolute top-1/2 left-2 size-4 -translate-y-1/2"
57
+ />
58
+ <Input
59
+ class="pl-8"
60
+ placeholder={t('providers.search')}
61
+ aria-label={t('providers.search')}
62
+ bind:value={query}
63
+ autocomplete="off"
64
+ />
65
+ </div>
66
+ <Select.Root
67
+ type="single"
68
+ value={filter}
69
+ onValueChange={(value) => (filter = value as typeof filter)}
70
+ >
71
+ <Select.Trigger
72
+ class={cn('shrink-0 px-2 [&>svg:last-child]:hidden', filter !== 'all' && 'bg-accent')}
73
+ aria-label={t('providers.filter')}
74
+ title={t(`providers.filter_${filter}`)}
75
+ >
76
+ <SlidersHorizontalIcon />
77
+ </Select.Trigger>
78
+ <Select.Content
79
+ ><Select.Group
80
+ >{#each FILTERS as f (f)}<Select.Item
81
+ value={f}
82
+ label={t(`providers.filter_${f}`)}
83
+ />{/each}</Select.Group
84
+ ></Select.Content
85
+ >
86
+ </Select.Root>
87
+ </div>
88
+ <ul class="flex min-w-0 flex-col gap-0.5">
89
+ {#each visible as p (p.id)}
90
+ {@const dot = status(p)}
91
+ <li
92
+ class="group/row relative"
93
+ oncontextmenu={(event) => {
94
+ event.preventDefault()
95
+ menuFor = p.id
96
+ }}
97
+ >
98
+ <button
99
+ type="button"
100
+ class={cn(
101
+ 'hover:bg-muted flex h-9 w-full min-w-0 items-center gap-2.5 rounded-kit px-2 pr-8 text-left text-kit-base',
102
+ selectedId === p.id && 'bg-muted font-medium',
103
+ )}
104
+ aria-current={selectedId === p.id ? 'true' : undefined}
105
+ onclick={() => onselect(p)}
106
+ >
107
+ <span
108
+ aria-hidden="true"
109
+ class="bg-primary/10 text-primary flex size-6 shrink-0 items-center justify-center rounded-md text-kit-xs font-semibold"
110
+ >{p.name.slice(0, 1).toUpperCase()}</span
111
+ >
112
+ <span class={cn('min-w-0 flex-1 truncate', !p.enabled && 'text-muted-foreground')}
113
+ >{p.name}</span
114
+ >
115
+ {#if dot}<span class={cn('size-1.5 shrink-0 rounded-full', dot)}></span><span
116
+ class="sr-only"
117
+ >{t(
118
+ dot === 'bg-success' ? 'providers.filter_enabled' : 'providers.needs_setup',
119
+ )}</span
120
+ >{/if}
121
+ </button>
122
+ <DropdownMenu.Root
123
+ open={menuFor === p.id}
124
+ onOpenChange={(open) => (menuFor = open ? p.id : null)}
125
+ >
126
+ <DropdownMenu.Trigger
127
+ >{#snippet child({ props })}<button
128
+ {...props}
129
+ type="button"
130
+ class={cn(
131
+ 'text-muted-foreground hover:text-foreground hover:bg-accent absolute top-1/2 right-1 inline-flex size-6 -translate-y-1/2 items-center justify-center rounded-md opacity-0 group-hover/row:opacity-100 focus-visible:opacity-100',
132
+ menuFor === p.id && 'opacity-100',
133
+ )}
134
+ aria-label={t('providers.row_menu', { name: p.name })}
135
+ title={t('providers.row_menu', { name: p.name })}
136
+ ><EllipsisIcon class="size-4" /></button
137
+ >{/snippet}</DropdownMenu.Trigger
138
+ >
139
+ <DropdownMenu.Content align="start" class="min-w-36">
140
+ <DropdownMenu.Item onSelect={() => onaction(p, 'edit')}
141
+ ><SquarePenIcon />{t('providers.edit')}</DropdownMenu.Item
142
+ >
143
+ <DropdownMenu.Item onSelect={() => onaction(p, 'notes')}
144
+ ><UserPenIcon />{t('providers.notes')}</DropdownMenu.Item
145
+ >
146
+ {#if !p.presetId}<DropdownMenu.Item
147
+ variant="destructive"
148
+ onSelect={() => onaction(p, 'delete')}
149
+ ><Trash2Icon />{t('providers.remove')}</DropdownMenu.Item
150
+ >{/if}
151
+ </DropdownMenu.Content>
152
+ </DropdownMenu.Root>
153
+ </li>
154
+ {:else}<li class="text-muted-foreground px-2 py-4 text-center text-kit-sm">
155
+ {t('providers.no_matches')}
156
+ </li>{/each}
157
+ </ul>
158
+ <Button variant="outline" class="mt-2" onclick={onadd}><PlusIcon />{t('providers.add')}</Button>
159
+ </nav>