@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,363 @@
1
+ <script lang="ts">
2
+ // External MCP server settings (docs/16 §4.4): status, start/stop, port, scope switches, tokens.
3
+ import { onDestroy } from 'svelte'
4
+ import { Alert, AlertDescription, AlertTitle } from '#lib/components/ui/alert/index.js'
5
+ import { Button } from '#lib/components/ui/button/index.js'
6
+ import * as Field from '#lib/components/ui/field/index.js'
7
+ import { Input } from '#lib/components/ui/input/index.js'
8
+ import { Switch } from '#lib/components/ui/switch/index.js'
9
+ import { getKitContext } from '../context'
10
+ import { isKitClientError } from '../ports'
11
+ import type { IssuedToken, ScopeState, Status, TokenRecord } from '../ports'
12
+ import { createClientSession } from '../session'
13
+ import { buildClaudePosixCommand, buildMcpJsonConfig } from './config'
14
+
15
+ /** Server key used in the copied client configs. */
16
+ let { serverName = 'aether' }: { serverName?: string } = $props()
17
+
18
+ const { ports, i18n, notify, logger } = getKitContext()
19
+ const server = ports.mcpServer
20
+ const t = i18n.t
21
+
22
+ let status = $state<Status | null>(null)
23
+ let scopes = $state<ScopeState[]>([])
24
+ let tokens = $state<TokenRecord[]>([])
25
+ let port = $state('')
26
+ let tokenName = $state('')
27
+ let tokenScopes = $state<string[]>([])
28
+ let issued = $state<IssuedToken | null>(null)
29
+ let busy = $state(false)
30
+ let issuing = $state(false)
31
+ const tokenSession = createClientSession()
32
+ let tokenRevision = 0
33
+
34
+ function clearIssued() {
35
+ tokenSession.invalidate()
36
+ issued = null
37
+ issuing = false
38
+ }
39
+
40
+ $effect(() => {
41
+ void serverName
42
+ clearIssued()
43
+ tokenName = ''
44
+ tokenScopes = []
45
+ tokenRevision++
46
+ })
47
+
48
+ onDestroy(() => {
49
+ clearIssued()
50
+ tokenName = ''
51
+ tokenScopes = []
52
+ })
53
+
54
+ function hasHttpUrl(value: string | undefined): value is string {
55
+ if (!value?.trim()) return false
56
+ try {
57
+ const url = new URL(value)
58
+ return url.protocol === 'http:' || url.protocol === 'https:'
59
+ } catch {
60
+ return false
61
+ }
62
+ }
63
+
64
+ // JSON accepts dynamic keys; Claude CLI names must also satisfy its naming rules.
65
+ const configInput = $derived.by(() => {
66
+ const url = status?.url
67
+ const token = issued?.token
68
+ if (!hasHttpUrl(url) || !serverName.trim() || !token?.trim()) return null
69
+ return { serverName, url, token }
70
+ })
71
+ const claudeNameValid = $derived(/^[A-Za-z0-9_-]+$/.test(serverName))
72
+
73
+ // Actions only render when the port exists; the throw marks a host wiring bug.
74
+ function need() {
75
+ if (!server) throw new Error('mcpServer port missing')
76
+ return server
77
+ }
78
+
79
+ function fail(error: unknown) {
80
+ const code = isKitClientError(error) ? error.code : 'internal'
81
+ logger.warn('mcp.server.ui_failed', { code })
82
+ notify.toast('error', t(`errors.${code}`))
83
+ }
84
+
85
+ async function act(run: () => Promise<void>) {
86
+ busy = true
87
+ try {
88
+ await run()
89
+ } catch (error) {
90
+ fail(error)
91
+ } finally {
92
+ busy = false
93
+ }
94
+ }
95
+
96
+ function setStatus(next: Status) {
97
+ if (status && status.url !== next.url) clearIssued()
98
+ status = next
99
+ port = String(next.port)
100
+ }
101
+
102
+ async function load() {
103
+ if (!server) return
104
+ const [nextStatus, nextScopes, nextTokens] = await Promise.all([
105
+ server.status(),
106
+ server.scopes(),
107
+ server.tokens(),
108
+ ])
109
+ setStatus(nextStatus)
110
+ scopes = nextScopes
111
+ tokens = nextTokens
112
+ }
113
+
114
+ $effect(() => {
115
+ void act(load)
116
+ })
117
+
118
+ const toggleRunning = () =>
119
+ act(async () => setStatus(status?.running ? await need().stop() : await need().start()))
120
+
121
+ const savePort = () => act(async () => setStatus(await need().setPort(Number(port))))
122
+
123
+ async function toggleScope(scope: ScopeState, on: boolean) {
124
+ if (on && scope.destructive) {
125
+ const ok = await notify.confirm({
126
+ title: t('mcp.server.destructiveTitle'),
127
+ body: t('mcp.server.destructiveBody', { scope: t(scope.labelKey) }),
128
+ danger: true,
129
+ })
130
+ if (!ok) return
131
+ }
132
+ const ids = scopes.filter((s) => (s.id === scope.id ? on : s.enabled)).map((s) => s.id)
133
+ await act(async () => {
134
+ scopes = await need().setScopes(ids)
135
+ })
136
+ }
137
+
138
+ function toggleTokenScope(id: string, on: boolean) {
139
+ tokenRevision++
140
+ tokenScopes = on ? [...tokenScopes, id] : tokenScopes.filter((s) => s !== id)
141
+ }
142
+
143
+ async function issue() {
144
+ clearIssued()
145
+ const token = tokenSession.capture()
146
+ const targetName = serverName
147
+ const targetUrl = status?.url
148
+ const revision = tokenRevision
149
+ const name = tokenName
150
+ const grants = [...tokenScopes]
151
+ const current = () =>
152
+ tokenSession.isCurrent(token) && targetName === serverName && targetUrl === status?.url
153
+ issuing = true
154
+ try {
155
+ const next = await need().issueToken(name, grants)
156
+ if (!current()) return
157
+ issued = next
158
+ if (revision === tokenRevision) {
159
+ tokenName = ''
160
+ tokenScopes = []
161
+ }
162
+ const nextTokens = await need().tokens()
163
+ if (current()) tokens = nextTokens
164
+ } catch (error) {
165
+ if (current()) fail(error)
166
+ } finally {
167
+ if (current()) issuing = false
168
+ }
169
+ }
170
+
171
+ const revoke = (token: TokenRecord) =>
172
+ act(async () => {
173
+ const ok = await notify.confirm({
174
+ title: t('mcp.server.revokeTitle'),
175
+ body: t('mcp.server.revokeBody', { name: token.name }),
176
+ danger: true,
177
+ })
178
+ if (!ok) return
179
+ if (issued?.id === token.id) clearIssued()
180
+ await need().revokeToken(token.id)
181
+ tokens = await need().tokens()
182
+ })
183
+
184
+ async function copy(format: 'json' | 'posix') {
185
+ const input = configInput
186
+ if (!input || (format === 'posix' && !claudeNameValid)) return
187
+ const token = tokenSession.capture()
188
+ const current = () =>
189
+ tokenSession.isCurrent(token) &&
190
+ serverName === input.serverName &&
191
+ status?.url === input.url &&
192
+ issued?.token === input.token
193
+ const text = format === 'json' ? buildMcpJsonConfig(input) : buildClaudePosixCommand(input)
194
+ if (!current()) return
195
+ try {
196
+ await navigator.clipboard.writeText(text)
197
+ if (current()) notify.toast('success', t('mcp.server.copied'))
198
+ } catch (error) {
199
+ if (current()) fail(error)
200
+ }
201
+ }
202
+
203
+ const date = (ms: number | null) => (ms === null ? '—' : new Date(ms).toLocaleString(i18n.locale))
204
+ </script>
205
+
206
+ {#if server && status}
207
+ <section class="flex flex-col gap-6" aria-label={t('mcp.server.title')}>
208
+ <Field.Group>
209
+ <Field.Field orientation="horizontal">
210
+ <Field.Content>
211
+ <Field.Label>{t('mcp.server.title')}</Field.Label>
212
+ <Field.Description>
213
+ {status.running
214
+ ? t('mcp.server.running', { url: status.url })
215
+ : t('mcp.server.stopped')}
216
+ · {t('mcp.server.toolCount', { count: status.toolCount })}
217
+ </Field.Description>
218
+ </Field.Content>
219
+ <Button
220
+ disabled={busy || issuing}
221
+ variant={status.running ? 'outline' : 'default'}
222
+ onclick={() => void toggleRunning()}
223
+ >
224
+ {status.running ? t('mcp.server.stop') : t('mcp.server.start')}
225
+ </Button>
226
+ </Field.Field>
227
+ <Field.Field orientation="horizontal">
228
+ <Field.Label for="kit-mcp-port">{t('mcp.server.port')}</Field.Label>
229
+ <Input
230
+ id="kit-mcp-port"
231
+ class="w-32"
232
+ type="number"
233
+ min="1024"
234
+ max="65535"
235
+ bind:value={port}
236
+ />
237
+ <Button
238
+ disabled={busy || issuing || port === String(status.port)}
239
+ variant="outline"
240
+ onclick={() => void savePort()}
241
+ >
242
+ {t('mcp.server.savePort')}
243
+ </Button>
244
+ </Field.Field>
245
+ </Field.Group>
246
+
247
+ <Field.Set>
248
+ <Field.Legend>{t('mcp.server.scopes')}</Field.Legend>
249
+ <Field.Description>{t('mcp.server.scopesHint')}</Field.Description>
250
+ <Field.Group>
251
+ {#each scopes as scope (scope.id)}
252
+ <Field.Field orientation="horizontal">
253
+ <Switch
254
+ id={`kit-mcp-scope-${scope.id}`}
255
+ bind:checked={() => scope.enabled, (on) => void toggleScope(scope, on)}
256
+ disabled={busy || issuing}
257
+ />
258
+ <Field.Label for={`kit-mcp-scope-${scope.id}`}
259
+ >{t(scope.labelKey)} ({scope.id})</Field.Label
260
+ >
261
+ </Field.Field>
262
+ {/each}
263
+ </Field.Group>
264
+ </Field.Set>
265
+
266
+ <Field.Set>
267
+ <Field.Legend>{t('mcp.server.newToken')}</Field.Legend>
268
+ <Field.Group>
269
+ <Field.Field>
270
+ <Field.Label for="kit-mcp-token-name">{t('mcp.server.tokenName')}</Field.Label>
271
+ <Input
272
+ id="kit-mcp-token-name"
273
+ bind:value={tokenName}
274
+ maxlength={200}
275
+ oninput={() => tokenRevision++}
276
+ />
277
+ </Field.Field>
278
+ {#each scopes as scope (scope.id)}
279
+ <Field.Field orientation="horizontal">
280
+ <Switch
281
+ id={`kit-mcp-grant-${scope.id}`}
282
+ checked={tokenScopes.includes(scope.id)}
283
+ onCheckedChange={(on) => toggleTokenScope(scope.id, on)}
284
+ />
285
+ <Field.Label for={`kit-mcp-grant-${scope.id}`}
286
+ >{t(scope.labelKey)} ({scope.id})</Field.Label
287
+ >
288
+ </Field.Field>
289
+ {/each}
290
+ <Field.Field orientation="horizontal">
291
+ <Button
292
+ disabled={busy || issuing || tokenName.trim() === '' || tokenScopes.length === 0}
293
+ onclick={() => void issue()}
294
+ >
295
+ {t('mcp.server.issue')}
296
+ </Button>
297
+ </Field.Field>
298
+ </Field.Group>
299
+ </Field.Set>
300
+
301
+ {#if issued}
302
+ <Alert>
303
+ <AlertTitle>{t('mcp.server.issuedTitle', { name: issued.name })}</AlertTitle>
304
+ <AlertDescription class="flex flex-col gap-2">
305
+ <p>{t('mcp.server.issuedOnce')}</p>
306
+ <code class="break-all">{issued.token}</code>
307
+ <div class="flex flex-wrap gap-2">
308
+ <Button
309
+ size="sm"
310
+ variant="outline"
311
+ disabled={!configInput || !claudeNameValid}
312
+ onclick={() => void copy('posix')}
313
+ >
314
+ {t('mcp.server.copyClaude')}
315
+ </Button>
316
+ <Button
317
+ size="sm"
318
+ variant="outline"
319
+ disabled={!configInput}
320
+ onclick={() => void copy('json')}
321
+ >
322
+ {t('mcp.server.copyJson')}
323
+ </Button>
324
+ <Button size="sm" variant="ghost" onclick={clearIssued}
325
+ >{t('mcp.server.dismiss')}</Button
326
+ >
327
+ </div>
328
+ </AlertDescription>
329
+ </Alert>
330
+ {/if}
331
+
332
+ <Field.Set>
333
+ <Field.Legend>{t('mcp.server.tokens')}</Field.Legend>
334
+ {#if tokens.length === 0}
335
+ <Field.Description>{t('mcp.server.noTokens')}</Field.Description>
336
+ {/if}
337
+ <ul class="flex flex-col gap-2">
338
+ {#each tokens as token (token.id)}
339
+ <li class="flex items-center justify-between gap-4 rounded-md border p-3">
340
+ <div class="flex min-w-0 flex-col">
341
+ <span class="truncate font-medium">{token.name}</span>
342
+ <span class="text-muted-foreground text-sm">
343
+ {token.scopes.join(', ')} · {t('mcp.server.lastUsed')}: {date(token.lastUsedAt)}
344
+ </span>
345
+ </div>
346
+ {#if token.revokedAt === null}
347
+ <Button
348
+ size="sm"
349
+ variant="destructive"
350
+ disabled={busy || issuing}
351
+ onclick={() => void revoke(token)}
352
+ >
353
+ {t('mcp.server.revoke')}
354
+ </Button>
355
+ {:else}
356
+ <span class="text-muted-foreground text-sm">{t('mcp.server.revoked')}</span>
357
+ {/if}
358
+ </li>
359
+ {/each}
360
+ </ul>
361
+ </Field.Set>
362
+ </section>
363
+ {/if}
@@ -0,0 +1,26 @@
1
+ interface McpConfigInput {
2
+ serverName: string
3
+ url: string
4
+ token: string
5
+ }
6
+
7
+ export function buildMcpJsonConfig({ serverName, url, token }: McpConfigInput): string {
8
+ return JSON.stringify(
9
+ {
10
+ mcpServers: {
11
+ [serverName]: { type: 'http', url, headers: { Authorization: `Bearer ${token}` } },
12
+ },
13
+ },
14
+ null,
15
+ 2,
16
+ )
17
+ }
18
+
19
+ function posixQuote(value: string): string {
20
+ return `'${value.replaceAll("'", "'\"'\"'")}'`
21
+ }
22
+
23
+ /** POSIX shell only: every dynamic value is exactly one quoted argument. */
24
+ export function buildClaudePosixCommand({ serverName, url, token }: McpConfigInput): string {
25
+ return `claude mcp add --transport http ${posixQuote(serverName)} ${posixQuote(url)} --header ${posixQuote(`Authorization: Bearer ${token}`)}`
26
+ }
@@ -0,0 +1,3 @@
1
+ // McpClientSettings、McpServerSettings。设计:docs/16 §4.4。
2
+ export { default as McpClientSettings } from './McpClientSettings.svelte'
3
+ export { default as McpServerSettings } from './McpServerSettings.svelte'
@@ -0,0 +1,74 @@
1
+ import type { McpTransport, ServerDto, ServerSave, ServerSecret } from '../ports'
2
+
3
+ /** Write-only editor input; credentials never come from a ServerDto. */
4
+ export interface McpInput {
5
+ name: string
6
+ slug: string
7
+ kind: McpTransport['kind']
8
+ url: string
9
+ command: string
10
+ args: string
11
+ cwd: string
12
+ replaceSecret: boolean
13
+ headers: string
14
+ query: string
15
+ env: string
16
+ }
17
+
18
+ export type McpInputResult =
19
+ { ok: true; value: ServerSave } | { ok: false; error: 'secretLine' | 'transportChanged' }
20
+
21
+ /** Split only at the first separator; values may themselves contain separators. */
22
+ function pairs(text: string, separator: string): Record<string, string> | null {
23
+ const entries: [string, string][] = []
24
+ for (const raw of text.split('\n')) {
25
+ const line = raw.trim()
26
+ if (!line) continue
27
+ const at = line.indexOf(separator)
28
+ const name = line.slice(0, at).trim()
29
+ if (at <= 0 || !name) return null
30
+ entries.push([name, line.slice(at + 1).trim()])
31
+ }
32
+ return Object.fromEntries(entries)
33
+ }
34
+
35
+ export function mapMcpInput(current: ServerDto | null, input: McpInput): McpInputResult {
36
+ if (current && current.transport.kind !== input.kind && !input.replaceSecret) {
37
+ return { ok: false, error: 'transportChanged' }
38
+ }
39
+ const transport: McpTransport =
40
+ input.kind === 'http'
41
+ ? { kind: 'http', url: input.url.trim() }
42
+ : {
43
+ kind: 'stdio',
44
+ command: input.command.trim(),
45
+ args: input.args.split('\n').filter((arg) => arg.trim() !== ''),
46
+ ...(input.cwd.trim() ? { cwd: input.cwd.trim() } : {}),
47
+ }
48
+ const value: ServerSave = {
49
+ ...(current ? { id: current.id } : {}),
50
+ name: input.name.trim(),
51
+ slug: input.slug.trim(),
52
+ transport,
53
+ enabled: current?.enabled ?? false,
54
+ trusted: current?.trusted ?? false,
55
+ allowPrivate: transport.kind === 'http' ? (current?.allowPrivate ?? false) : false,
56
+ }
57
+ if (input.replaceSecret) {
58
+ const secret: ServerSecret = {}
59
+ if (input.kind === 'http') {
60
+ const headers = pairs(input.headers, ':')
61
+ const query = pairs(input.query, '=')
62
+ if (!headers || !query) return { ok: false, error: 'secretLine' }
63
+ if (Object.keys(headers).length) secret.headers = headers
64
+ if (Object.keys(query).length) secret.query = query
65
+ } else {
66
+ const env = pairs(input.env, '=')
67
+ if (!env) return { ok: false, error: 'secretLine' }
68
+ if (Object.keys(env).length) secret.env = env
69
+ }
70
+ // Omitted preserves; explicit {} clears. New servers need no empty secret.
71
+ if (current || Object.keys(secret).length) value.secret = secret
72
+ }
73
+ return { ok: true, value }
74
+ }
@@ -0,0 +1,43 @@
1
+ // Default KitNotify for hosts without their own: svelte-sonner toasts + one shadcn AlertDialog (docs/16 §3).
2
+ // Render <KitNotifier notify={...} /> once next to the host root so the toaster and dialog exist.
3
+ import { toast } from 'svelte-sonner'
4
+ import type { ConfirmOptions, KitNotify } from './context'
5
+
6
+ export class DefaultNotify implements KitNotify {
7
+ #request = $state<ConfirmOptions | null>(null)
8
+ #open = $state(false)
9
+ #resolve: ((answer: boolean) => void) | null = null
10
+
11
+ /** Last request; kept after closing so the dialog keeps its text while it animates out. */
12
+ get request(): ConfirmOptions | null {
13
+ return this.#request
14
+ }
15
+
16
+ get open(): boolean {
17
+ return this.#open
18
+ }
19
+
20
+ toast(kind: 'info' | 'success' | 'error', text: string): void {
21
+ toast[kind](text)
22
+ }
23
+
24
+ /** A newer confirm answers the pending one with `false`. */
25
+ confirm(options: ConfirmOptions): Promise<boolean> {
26
+ this.answer(false)
27
+ this.#request = options
28
+ this.#open = true
29
+ const { promise, resolve } = Promise.withResolvers<boolean>()
30
+ this.#resolve = resolve
31
+ return promise
32
+ }
33
+
34
+ answer(isConfirmed: boolean): void {
35
+ this.#open = false
36
+ this.#resolve?.(isConfirmed)
37
+ this.#resolve = null
38
+ }
39
+ }
40
+
41
+ export function createDefaultNotify(): DefaultNotify {
42
+ return new DefaultNotify()
43
+ }
@@ -0,0 +1,174 @@
1
+ // Port interfaces and KitClientError (docs/16 §2, docs/04 §3.2). Pure TS: no Tauri imports.
2
+ import type {
3
+ Conversation,
4
+ ConversationSummary,
5
+ IngestBatch,
6
+ IngestReceipt,
7
+ IssuedToken,
8
+ KitError,
9
+ KitErrorCode,
10
+ Details,
11
+ EndpointKeySave,
12
+ LogEntry,
13
+ LogLevel,
14
+ LogStatus,
15
+ ManualSave,
16
+ Approval,
17
+ ModelFilter,
18
+ ModelOption,
19
+ ProbeResult,
20
+ Preview,
21
+ ProbeTarget,
22
+ Provider,
23
+ ProviderSave,
24
+ Protocol,
25
+ RunAccepted,
26
+ RunEvent,
27
+ RunRequest,
28
+ RunSnapshot,
29
+ ServerDto,
30
+ ServerSave,
31
+ SkillDto,
32
+ SkillMeta,
33
+ Source,
34
+ ScopeState,
35
+ Status,
36
+ TokenRecord,
37
+ ToolDto,
38
+ } from '@ouraihub/aether-contracts'
39
+ import type { SearchPort } from './search'
40
+ import type { SkillSourcesPort } from './skill-sources'
41
+
42
+ export interface CallOptions {
43
+ signal?: AbortSignal
44
+ }
45
+
46
+ export interface LogsPort {
47
+ tail(limit: number): Promise<LogEntry[]>
48
+ ingest(batch: IngestBatch): Promise<IngestReceipt>
49
+ getLevel(): Promise<LogStatus>
50
+ /** Takes effect immediately and is persisted to `level.json`. */
51
+ setLevel(level: LogLevel): Promise<LogStatus>
52
+ }
53
+
54
+ export interface DesktopActions {
55
+ openLogsDirectory(): Promise<void>
56
+ }
57
+
58
+ /** Host-supplied provider template, seeded once into the store disabled and keyless (docs/39). */
59
+ export interface ProviderPreset {
60
+ id: string
61
+ name: string
62
+ baseUrl: string
63
+ protocol: Protocol
64
+ /** "Get a key" link, e.g. an invite link. */
65
+ apiKeyUrl?: string
66
+ }
67
+
68
+ export interface ProvidersPort {
69
+ /** Browser asks for the current password; desktop uses its local credential boundary. */
70
+ readonly revealNeedsPassword: boolean
71
+ /** Host stores `keyStrategy` and key set-aside state (web); `false` = rotation only, fields ignored. */
72
+ readonly keyStrategies: boolean
73
+ list(): Promise<Provider[]>
74
+ setEnabled(id: string, enabled: boolean): Promise<Provider[]>
75
+ save(provider: ProviderSave): Promise<Provider[]>
76
+ remove(id: string): Promise<Provider[]>
77
+ modelOptions(filter?: ModelFilter): Promise<ModelOption[]>
78
+ probe(target: ProbeTarget, options?: CallOptions): Promise<ProbeResult>
79
+ saveKey(input: EndpointKeySave): Promise<Provider[]>
80
+ removeKey(endpointId: string, keyId: string): Promise<Provider[]>
81
+ revealKey(keyId: string, currentPassword?: string | null): Promise<string>
82
+ }
83
+
84
+ export interface ConversationClient {
85
+ list(scope?: string): Promise<ConversationSummary[]>
86
+ get(id: string): Promise<Conversation>
87
+ remove(id: string): Promise<void>
88
+ rename(id: string, title: string): Promise<ConversationSummary>
89
+ editTurn(
90
+ conversationId: string,
91
+ turnId: string,
92
+ text: string,
93
+ expectedText: string,
94
+ ): Promise<Conversation>
95
+ removeTurn(conversationId: string, turnId: string): Promise<Conversation>
96
+ fork(conversationId: string, turnId: string): Promise<Conversation>
97
+ selectTurn(conversationId: string, turnId: string): Promise<Conversation>
98
+ }
99
+
100
+ export interface RunClient {
101
+ /** Resolves once accepted; events may arrive before acceptance. */
102
+ start(
103
+ request: RunRequest,
104
+ onEvent: (event: RunEvent) => void,
105
+ options?: CallOptions,
106
+ ): Promise<RunAccepted>
107
+ cancel(runId: string): Promise<void>
108
+ approve(runId: string, callId: string, allow: boolean): Promise<void>
109
+ getSnapshot(runId: string): Promise<RunSnapshot>
110
+ }
111
+
112
+ export interface SkillsPort {
113
+ list(): Promise<SkillMeta[]>
114
+ get(id: string): Promise<SkillDto>
115
+ /** Reads and validates a package; nothing is stored. */
116
+ preview(source: Source): Promise<Preview>
117
+ /** `confirmedDigest` is the previewed digest; a changed package rejects with `conflict{content_changed}`. */
118
+ install(source: Source, confirmedDigest: string, replace: boolean): Promise<SkillMeta[]>
119
+ save(input: ManualSave): Promise<SkillMeta[]>
120
+ duplicate(id: string, newName: string): Promise<SkillMeta[]>
121
+ reorder(ids: readonly string[]): Promise<SkillMeta[]>
122
+ remove(id: string): Promise<SkillMeta[]>
123
+ }
124
+
125
+ export interface McpClientPort {
126
+ list(): Promise<ServerDto[]>
127
+ save(server: ServerSave): Promise<ServerDto[]>
128
+ remove(id: string): Promise<ServerDto[]>
129
+ refresh(serverId: string): Promise<ToolDto[]>
130
+ setToolApproval(serverId: string, tool: string, approval: Approval): Promise<void>
131
+ }
132
+
133
+ export interface McpServerPort {
134
+ status(): Promise<Status>
135
+ tokens(): Promise<TokenRecord[]>
136
+ start(): Promise<Status>
137
+ stop(): Promise<Status>
138
+ setPort(port: number): Promise<Status>
139
+ issueToken(name: string, scopes: readonly string[]): Promise<IssuedToken>
140
+ revokeToken(id: string): Promise<void>
141
+ scopes(): Promise<ScopeState[]>
142
+ setScopes(ids: readonly string[]): Promise<ScopeState[]>
143
+ }
144
+
145
+ /** Ports the host declared; components skip the area of a missing port (docs/16 §2). */
146
+ export interface KitPorts {
147
+ logs?: LogsPort
148
+ desktop?: DesktopActions
149
+ providers?: ProvidersPort
150
+ chat?: RunClient
151
+ conversations?: ConversationClient
152
+ search?: SearchPort
153
+ skills?: SkillsPort
154
+ skillSources?: SkillSourcesPort
155
+ mcpClient?: McpClientPort
156
+ mcpServer?: McpServerPort
157
+ }
158
+
159
+ /** The only error type thrown across a port (docs/04 §3.2). */
160
+ export class KitClientError extends Error {
161
+ readonly code: KitErrorCode
162
+ readonly details: Details
163
+
164
+ constructor(error: KitError) {
165
+ super(error.message)
166
+ this.name = 'KitClientError'
167
+ this.code = error.code
168
+ this.details = error.details ?? {}
169
+ }
170
+ }
171
+
172
+ export function isKitClientError(error: unknown): error is KitClientError {
173
+ return error instanceof KitClientError
174
+ }