@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,418 @@
1
+ <script lang="ts">
2
+ // External MCP servers the in-app AI may call (docs/16 §4.4, docs/13 §2): list, edit with
3
+ // write-only credentials, test connection, trust/enable/private-network switches, and each
4
+ // tool's approval default. Only enabled + trusted servers' tools reach a run. A server is
5
+ // reached over Streamable HTTP or as a local stdio command.
6
+ import { onDestroy } from 'svelte'
7
+ import { Alert, AlertDescription } from '#lib/components/ui/alert/index.js'
8
+ import { Button } from '#lib/components/ui/button/index.js'
9
+ import * as Empty from '#lib/components/ui/empty/index.js'
10
+ import * as Field from '#lib/components/ui/field/index.js'
11
+ import * as Select from '#lib/components/ui/select/index.js'
12
+ import { Switch } from '#lib/components/ui/switch/index.js'
13
+ import McpClientEditor from './McpClientEditor.svelte'
14
+ import { getKitContext } from '../context'
15
+ import { isKitClientError } from '../ports'
16
+ import type { Approval, McpTransport, ServerDto, ServerSave } from '../ports'
17
+ import { createClientSession } from '../session'
18
+ import { mapMcpInput } from './input'
19
+
20
+ const APPROVALS: Approval[] = ['auto', 'ask', 'deny']
21
+
22
+ const { ports, i18n, notify, logger } = getKitContext()
23
+ const client = ports.mcpClient
24
+ const t = i18n.t
25
+
26
+ let servers = $state<ServerDto[]>([])
27
+ let busy = $state(false)
28
+ /** Last "test connection" failure per server id. */
29
+ let testErrors = $state<Record<string, string>>({})
30
+
31
+ let editing = $state(false)
32
+ let editId = $state<string | null>(null)
33
+ let name = $state('')
34
+ let slug = $state('')
35
+ let kind = $state<McpTransport['kind']>('http')
36
+ let url = $state('')
37
+ let command = $state('')
38
+ let args = $state('')
39
+ let cwd = $state('')
40
+ let replaceSecret = $state(true)
41
+ let headers = $state('')
42
+ let query = $state('')
43
+ let env = $state('')
44
+ let currentKeys = $state<string[]>([])
45
+ const editSession = createClientSession()
46
+ let editTarget: ServerDto | null = null
47
+ let editorSaving = $state(false)
48
+ let editRevision = 0
49
+ let secretRevision = 0
50
+
51
+ function clearSecrets() {
52
+ headers = ''
53
+ query = ''
54
+ env = ''
55
+ secretRevision++
56
+ }
57
+
58
+ function invalidateEditor() {
59
+ editSession.invalidate()
60
+ editorSaving = false
61
+ clearSecrets()
62
+ }
63
+
64
+ function closeEditor() {
65
+ invalidateEditor()
66
+ editing = false
67
+ editTarget = null
68
+ }
69
+
70
+ function changeKind(next: McpTransport['kind']) {
71
+ if (next === kind) return
72
+ invalidateEditor()
73
+ editRevision++
74
+ kind = next
75
+ }
76
+
77
+ function changeReplacement(next: boolean) {
78
+ invalidateEditor()
79
+ editRevision++
80
+ replaceSecret = next
81
+ }
82
+
83
+ onDestroy(closeEditor)
84
+
85
+ // Actions only render when the port exists; the throw marks a host wiring bug.
86
+ function need() {
87
+ if (!client) throw new Error('mcpClient port missing')
88
+ return client
89
+ }
90
+
91
+ function errorText(error: unknown): string {
92
+ if (!isKitClientError(error)) return t('errors.internal')
93
+ const { reason, field } = error.details
94
+ if (reason === 'credential_query') return t('mcp.client.err.credentialQuery')
95
+ if (reason === 'duplicate_slug') return t('mcp.client.err.duplicateSlug')
96
+ if (reason === 'credential_arg') return t('mcp.client.err.credentialArg')
97
+ if (reason === 'transport_changed') return t('mcp.client.err.transportChanged')
98
+ return field ? `${t(`errors.${error.code}`)} (${field})` : t(`errors.${error.code}`)
99
+ }
100
+
101
+ function fail(error: unknown) {
102
+ logger.warn('mcp.client.ui_failed', { code: isKitClientError(error) ? error.code : 'internal' })
103
+ notify.toast('error', errorText(error))
104
+ }
105
+
106
+ async function act(run: () => Promise<void>) {
107
+ busy = true
108
+ try {
109
+ await run()
110
+ } catch (error) {
111
+ fail(error)
112
+ } finally {
113
+ busy = false
114
+ }
115
+ }
116
+
117
+ $effect(() => {
118
+ if (client) void act(async () => void (servers = await client.list()))
119
+ })
120
+
121
+ function base(server: ServerDto): ServerSave {
122
+ const { id, slug, name, transport, enabled, trusted, allowPrivate } = server
123
+ return { id, slug, name, transport, enabled, trusted, allowPrivate }
124
+ }
125
+
126
+ /** One-line location shown in the list: the URL, or the command line. */
127
+ function location(transport: McpTransport): string {
128
+ return transport.kind === 'http'
129
+ ? transport.url
130
+ : [transport.command, ...transport.args].join(' ')
131
+ }
132
+
133
+ const update = (next: ServerSave) => act(async () => void (servers = await need().save(next)))
134
+
135
+ async function setTrusted(server: ServerDto, on: boolean) {
136
+ if (on) {
137
+ const ok = await notify.confirm({
138
+ title: t('mcp.client.trustTitle'),
139
+ body: t('mcp.client.trustBody', { name: server.name }),
140
+ })
141
+ if (!ok) return
142
+ }
143
+ await update({ ...base(server), trusted: on })
144
+ }
145
+
146
+ async function setPrivate(server: ServerDto, on: boolean) {
147
+ if (on) {
148
+ const ok = await notify.confirm({
149
+ title: t('mcp.client.privateTitle'),
150
+ body: t('mcp.client.privateBody', { url: location(server.transport) }),
151
+ danger: true,
152
+ })
153
+ if (!ok) return
154
+ }
155
+ await update({ ...base(server), allowPrivate: on })
156
+ }
157
+
158
+ const test = (server: ServerDto) =>
159
+ act(async () => {
160
+ testErrors = Object.fromEntries(Object.entries(testErrors).filter(([id]) => id !== server.id))
161
+ try {
162
+ await need().refresh(server.id)
163
+ } catch (error) {
164
+ testErrors = { ...testErrors, [server.id]: errorText(error) }
165
+ }
166
+ servers = await need().list()
167
+ })
168
+
169
+ const setApproval = (server: ServerDto, tool: string, approval: Approval) =>
170
+ act(async () => {
171
+ await need().setToolApproval(server.id, tool, approval)
172
+ servers = await need().list()
173
+ })
174
+
175
+ const remove = (server: ServerDto) =>
176
+ act(async () => {
177
+ const ok = await notify.confirm({
178
+ title: t('mcp.client.deleteTitle'),
179
+ body: t('mcp.client.deleteBody', { name: server.name }),
180
+ danger: true,
181
+ })
182
+ if (ok) servers = await need().remove(server.id)
183
+ })
184
+
185
+ function edit(server: ServerDto | null) {
186
+ invalidateEditor()
187
+ editTarget = server
188
+ editRevision++
189
+ const transport = server?.transport
190
+ editId = server?.id ?? null
191
+ name = server?.name ?? ''
192
+ slug = server?.slug ?? ''
193
+ kind = transport?.kind ?? 'http'
194
+ url = transport?.kind === 'http' ? transport.url : ''
195
+ command = transport?.kind === 'stdio' ? transport.command : ''
196
+ args = transport?.kind === 'stdio' ? transport.args.join('\n') : ''
197
+ cwd = transport?.kind === 'stdio' ? (transport.cwd ?? '') : ''
198
+ currentKeys = server?.secretKeys ?? []
199
+ replaceSecret = server === null
200
+ editing = true
201
+ }
202
+
203
+ async function save() {
204
+ const mapped = mapMcpInput(editTarget, {
205
+ name,
206
+ slug,
207
+ kind,
208
+ url,
209
+ command,
210
+ args,
211
+ cwd,
212
+ replaceSecret,
213
+ headers,
214
+ query,
215
+ env,
216
+ })
217
+ if (!mapped.ok) {
218
+ notify.toast('error', t(`mcp.client.err.${mapped.error}`))
219
+ return
220
+ }
221
+ const token = editSession.capture()
222
+ const revision = editRevision
223
+ const sentSecretRevision = secretRevision
224
+ editorSaving = true
225
+ try {
226
+ const next = await need().save(mapped.value)
227
+ if (!editSession.isCurrent(token)) return
228
+ servers = next
229
+ if (editRevision === revision) {
230
+ closeEditor()
231
+ } else {
232
+ // Preserve edits made while saving, but do not retain successfully sent secrets.
233
+ const stored = editTarget
234
+ ? next.find((server) => server.id === editTarget?.id)
235
+ : next.find((server) => server.slug === mapped.value.slug)
236
+ if (stored) {
237
+ editTarget = stored
238
+ editId = stored.id
239
+ currentKeys = stored.secretKeys
240
+ }
241
+ if (sentSecretRevision === secretRevision) {
242
+ clearSecrets()
243
+ replaceSecret = false
244
+ }
245
+ }
246
+ } catch (error) {
247
+ if (editSession.isCurrent(token)) fail(error)
248
+ } finally {
249
+ if (editSession.isCurrent(token)) editorSaving = false
250
+ }
251
+ }
252
+ </script>
253
+
254
+ {#if client}
255
+ <section class="flex flex-col gap-4" aria-label={t('mcp.client.title')}>
256
+ <div class="flex items-center gap-2">
257
+ <p class="text-muted-foreground flex-1 text-sm">{t('mcp.client.hint')}</p>
258
+ <Button disabled={busy} onclick={() => edit(null)}>{t('mcp.client.add')}</Button>
259
+ </div>
260
+ {#if servers.length === 0}
261
+ <Empty.Root>
262
+ <Empty.Header>
263
+ <Empty.Title>{t('mcp.client.empty')}</Empty.Title>
264
+ <Empty.Description>{t('mcp.client.emptyHint')}</Empty.Description>
265
+ </Empty.Header>
266
+ </Empty.Root>
267
+ {/if}
268
+ <ul class="flex flex-col gap-3">
269
+ {#each servers as server (server.id)}
270
+ <li class="flex flex-col gap-3 rounded-md border p-3" aria-label={server.name}>
271
+ <div class="flex items-start gap-3">
272
+ <div class="flex min-w-0 flex-1 flex-col">
273
+ <span class="truncate font-medium"
274
+ >{server.name} <span class="text-muted-foreground">({server.slug})</span></span
275
+ >
276
+ <span class="text-muted-foreground truncate font-mono text-xs"
277
+ >{t(`mcp.client.transport.${server.transport.kind}`)} · {location(
278
+ server.transport,
279
+ )}</span
280
+ >
281
+ {#if server.secretKeys.length}
282
+ <span class="text-muted-foreground text-xs">
283
+ {t('mcp.client.secretKeys', { keys: server.secretKeys.join(', ') })}
284
+ </span>
285
+ {/if}
286
+ </div>
287
+ <Button size="sm" variant="outline" disabled={busy} onclick={() => void test(server)}>
288
+ {t('mcp.client.test')}
289
+ </Button>
290
+ <Button size="sm" variant="outline" disabled={busy} onclick={() => edit(server)}>
291
+ {t('mcp.client.edit')}
292
+ </Button>
293
+ <Button
294
+ size="sm"
295
+ variant="destructive"
296
+ disabled={busy}
297
+ onclick={() => void remove(server)}
298
+ >
299
+ {t('mcp.client.delete')}
300
+ </Button>
301
+ </div>
302
+ <div class="flex flex-wrap gap-6">
303
+ <Field.Field orientation="horizontal" class="w-auto">
304
+ <Switch
305
+ id={`kit-mcp-enabled-${server.id}`}
306
+ checked={server.enabled}
307
+ disabled={busy}
308
+ onCheckedChange={(on) => void update({ ...base(server), enabled: on })}
309
+ />
310
+ <Field.Label for={`kit-mcp-enabled-${server.id}`}
311
+ >{t('mcp.client.enabled')}</Field.Label
312
+ >
313
+ </Field.Field>
314
+ <Field.Field orientation="horizontal" class="w-auto">
315
+ <Switch
316
+ id={`kit-mcp-trusted-${server.id}`}
317
+ checked={server.trusted}
318
+ disabled={busy}
319
+ onCheckedChange={(on) => void setTrusted(server, on)}
320
+ />
321
+ <Field.Label for={`kit-mcp-trusted-${server.id}`}
322
+ >{t('mcp.client.trusted')}</Field.Label
323
+ >
324
+ </Field.Field>
325
+ {#if server.transport.kind === 'http'}
326
+ <Field.Field orientation="horizontal" class="w-auto">
327
+ <Switch
328
+ id={`kit-mcp-private-${server.id}`}
329
+ checked={server.allowPrivate}
330
+ disabled={busy}
331
+ onCheckedChange={(on) => void setPrivate(server, on)}
332
+ />
333
+ <Field.Label for={`kit-mcp-private-${server.id}`}
334
+ >{t('mcp.client.allowPrivate')}</Field.Label
335
+ >
336
+ </Field.Field>
337
+ {/if}
338
+ </div>
339
+ {#if testErrors[server.id]}
340
+ <Alert variant="destructive">
341
+ <AlertDescription
342
+ >{t('mcp.client.testFailed', {
343
+ error: testErrors[server.id] ?? '',
344
+ })}</AlertDescription
345
+ >
346
+ </Alert>
347
+ {/if}
348
+ {#if server.tools.length}
349
+ <ul class="flex flex-col gap-2" aria-label={t('mcp.client.tools')}>
350
+ {#each server.tools as tool (tool.name)}
351
+ <li class="flex items-center gap-3">
352
+ <div class="flex min-w-0 flex-1 flex-col">
353
+ <span class="truncate font-mono text-sm">{tool.name}</span>
354
+ <span class="text-muted-foreground truncate text-xs">
355
+ {tool.description}{#if tool.readOnly && tool.approval !== 'auto'}
356
+ · {t('mcp.client.readOnlyHint')}{/if}
357
+ </span>
358
+ </div>
359
+ <Select.Root
360
+ type="single"
361
+ value={tool.approval}
362
+ onValueChange={(value) =>
363
+ void setApproval(server, tool.name, value as Approval)}
364
+ >
365
+ <Select.Trigger
366
+ size="sm"
367
+ class="w-28"
368
+ aria-label={t('mcp.client.approvalFor', { tool: tool.name })}
369
+ >
370
+ {t(`mcp.client.approval.${tool.approval}`)}
371
+ </Select.Trigger>
372
+ <Select.Content>
373
+ <Select.Group>
374
+ {#each APPROVALS as approval (approval)}
375
+ <Select.Item
376
+ value={approval}
377
+ label={t(`mcp.client.approval.${approval}`)}
378
+ />
379
+ {/each}
380
+ </Select.Group>
381
+ </Select.Content>
382
+ </Select.Root>
383
+ </li>
384
+ {/each}
385
+ </ul>
386
+ {:else}
387
+ <p class="text-muted-foreground text-sm">{t('mcp.client.noTools')}</p>
388
+ {/if}
389
+ </li>
390
+ {/each}
391
+ </ul>
392
+ </section>
393
+
394
+ <McpClientEditor
395
+ bind:open={editing}
396
+ {editId}
397
+ bind:name
398
+ bind:slug
399
+ bind:kind
400
+ bind:url
401
+ bind:command
402
+ bind:args
403
+ bind:cwd
404
+ bind:replaceSecret
405
+ bind:headers
406
+ bind:query
407
+ bind:env
408
+ {currentKeys}
409
+ {editorSaving}
410
+ {busy}
411
+ onclose={closeEditor}
412
+ onsave={save}
413
+ onchangeKind={changeKind}
414
+ onchangeReplacement={changeReplacement}
415
+ oninputRevision={() => editRevision++}
416
+ onsecretInput={() => secretRevision++}
417
+ />
418
+ {/if}