@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,103 @@
1
+ <script lang="ts">
2
+ // Log viewer (docs/16 §4.5): tail(300) every 2 s while the page is visible; problems-only filter; open folder.
3
+ import { Alert, AlertDescription } from '#lib/components/ui/alert/index.js'
4
+ import { Button } from '#lib/components/ui/button/index.js'
5
+ import * as Empty from '#lib/components/ui/empty/index.js'
6
+ import { Switch } from '#lib/components/ui/switch/index.js'
7
+ import { getKitContext } from '../context'
8
+ import type { LogEntry } from '../ports'
9
+ import { cn } from '../utils'
10
+
11
+ const POLL_MS = 2000
12
+ const LIMIT = 300
13
+
14
+ const { ports, i18n, notify } = getKitContext()
15
+ const t = i18n.t
16
+ const logs = ports.logs
17
+
18
+ let entries = $state<LogEntry[]>([])
19
+ let failed = $state(false)
20
+ let problemsOnly = $state(false)
21
+ const shown = $derived(
22
+ problemsOnly ? entries.filter((e) => e.level === 'ERROR' || e.level === 'WARN') : entries,
23
+ )
24
+
25
+ async function refresh() {
26
+ if (!logs) return
27
+ try {
28
+ entries = await logs.tail(LIMIT)
29
+ failed = false
30
+ } catch {
31
+ failed = true
32
+ }
33
+ }
34
+
35
+ $effect(() => {
36
+ if (!logs) return
37
+ let timer: number | undefined
38
+ const sync = () => {
39
+ window.clearInterval(timer)
40
+ timer = undefined
41
+ if (document.visibilityState !== 'visible') return
42
+ void refresh()
43
+ timer = window.setInterval(() => void refresh(), POLL_MS)
44
+ }
45
+ sync()
46
+ document.addEventListener('visibilitychange', sync)
47
+ return () => {
48
+ window.clearInterval(timer)
49
+ document.removeEventListener('visibilitychange', sync)
50
+ }
51
+ })
52
+
53
+ async function openDir() {
54
+ try {
55
+ await ports.desktop?.openLogsDirectory()
56
+ } catch {
57
+ notify.toast('error', t('errors.internal'))
58
+ }
59
+ }
60
+ </script>
61
+
62
+ {#if logs}
63
+ <section class="flex min-h-0 flex-1 flex-col gap-3" aria-label={t('logs.title')}>
64
+ <header class="flex items-center gap-3">
65
+ <h2 class="text-base font-medium">{t('logs.title')}</h2>
66
+ <label class="ml-auto flex items-center gap-2 text-sm">
67
+ <Switch bind:checked={problemsOnly} />
68
+ {t('logs.problemsOnly')}
69
+ </label>
70
+ {#if ports.desktop}
71
+ <Button variant="outline" size="sm" onclick={() => void openDir()}
72
+ >{t('logs.openDir')}</Button
73
+ >
74
+ {/if}
75
+ </header>
76
+ {#if failed}
77
+ <Alert variant="destructive"
78
+ ><AlertDescription>{t('logs.loadFailed')}</AlertDescription></Alert
79
+ >
80
+ {/if}
81
+ {#if shown.length === 0}
82
+ <Empty.Root>
83
+ <Empty.Header><Empty.Title>{t('logs.empty')}</Empty.Title></Empty.Header>
84
+ </Empty.Root>
85
+ {:else}
86
+ <ol class="bg-muted min-h-0 flex-1 overflow-auto rounded-md p-2 font-mono text-xs">
87
+ {#each shown as entry (entry.seq)}
88
+ <li
89
+ class={cn(
90
+ 'break-all whitespace-pre-wrap',
91
+ entry.level === 'ERROR' && 'text-destructive',
92
+ (entry.level === 'DEBUG' || entry.level === 'TRACE') && 'text-muted-foreground',
93
+ )}
94
+ >
95
+ {entry.time}
96
+ {entry.level}
97
+ {entry.target}: {entry.message}
98
+ </li>
99
+ {/each}
100
+ </ol>
101
+ {/if}
102
+ </section>
103
+ {/if}
@@ -0,0 +1,58 @@
1
+ <script lang="ts">
2
+ // Log level (docs/16 §4.5): applies immediately; warns when the log file cannot be written.
3
+ import { Alert, AlertDescription } from '#lib/components/ui/alert/index.js'
4
+ import * as Field from '#lib/components/ui/field/index.js'
5
+ import * as Select from '#lib/components/ui/select/index.js'
6
+ import { getKitContext } from '../context'
7
+ import { isKitClientError } from '../ports'
8
+ import type { LogLevel, LogStatus } from '../ports'
9
+
10
+ const LEVELS: readonly LogLevel[] = ['ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE']
11
+
12
+ const { ports, i18n, notify } = getKitContext()
13
+ const t = i18n.t
14
+ const logs = ports.logs
15
+
16
+ let status = $state<LogStatus | null>(null)
17
+
18
+ async function run(call: () => Promise<LogStatus>) {
19
+ try {
20
+ status = await call()
21
+ } catch (error) {
22
+ notify.toast('error', t(`errors.${isKitClientError(error) ? error.code : 'internal'}`))
23
+ }
24
+ }
25
+
26
+ $effect(() => {
27
+ if (logs) void run(() => logs.getLevel())
28
+ })
29
+ </script>
30
+
31
+ {#if logs}
32
+ <Field.Group>
33
+ <Field.Field>
34
+ <Field.Label for="kit-log-level">{t('logs.level')}</Field.Label>
35
+ <Select.Root
36
+ type="single"
37
+ value={status?.level ?? ''}
38
+ disabled={!status}
39
+ onValueChange={(value) => void run(() => logs.setLevel(value as LogLevel))}
40
+ >
41
+ <Select.Trigger id="kit-log-level" class="w-40">{status?.level ?? '—'}</Select.Trigger>
42
+ <Select.Content>
43
+ <Select.Group>
44
+ {#each LEVELS as level (level)}
45
+ <Select.Item value={level} label={level} />
46
+ {/each}
47
+ </Select.Group>
48
+ </Select.Content>
49
+ </Select.Root>
50
+ <Field.Description>{t('logs.levelHint')}</Field.Description>
51
+ </Field.Field>
52
+ {#if status && !status.available}
53
+ <Alert variant="destructive"
54
+ ><AlertDescription>{t('logs.unavailable')}</AlertDescription></Alert
55
+ >
56
+ {/if}
57
+ </Field.Group>
58
+ {/if}
@@ -0,0 +1,5 @@
1
+ // LogPanel、LogSettings、createLogger。设计:docs/16 §4.5。
2
+ export { createLogger } from './logger'
3
+ export type { BatchingLogger } from './logger'
4
+ export { default as LogPanel } from './LogPanel.svelte'
5
+ export { default as LogSettings } from './LogSettings.svelte'
@@ -0,0 +1,96 @@
1
+ // createLogger (docs/16 §4.5): same whitelist as the backend (docs/10 §10), batched into LogsPort.ingest.
2
+ // ponytail: secret detection stays in the backend (it rejects and counts); mirror mask.rs here
3
+ // if a secret-looking value must never leave the page.
4
+ import type { KitLogger, LogFields } from '../context'
5
+ import type { IngestEntry, IngestLevel, LogsPort } from '../ports'
6
+
7
+ const MAX_QUEUE = 200
8
+ const BATCH = 20
9
+ const MAX_BATCH = 50
10
+ const DELAY_MS = 250
11
+ const MAX_FIELDS = 16
12
+ const MAX_STRING = 200
13
+ const EVENT = /^[a-z][a-z0-9_]*(\.[a-z0-9_]+){1,5}$/
14
+ const KEY = /^[a-z][a-z0-9_]*(\.[a-z0-9_]+){0,5}$/
15
+
16
+ export interface BatchingLogger extends KitLogger {
17
+ /** Sends everything queued now (also done on `pagehide`). */
18
+ flush(): Promise<void>
19
+ }
20
+
21
+ function isValid(event: string, fields: LogFields | undefined): boolean {
22
+ if (!EVENT.test(event)) return false
23
+ const entries = Object.entries(fields ?? {})
24
+ return (
25
+ entries.length <= MAX_FIELDS &&
26
+ entries.every(([key, value]) => {
27
+ if (!KEY.test(key)) return false
28
+ if (typeof value === 'string') return [...value].length <= MAX_STRING
29
+ return typeof value === 'boolean' || Number.isFinite(value)
30
+ })
31
+ )
32
+ }
33
+
34
+ export function createLogger(port: LogsPort): BatchingLogger {
35
+ const queue: IngestEntry[] = []
36
+ let dropped = 0
37
+ let timer: number | undefined
38
+ let inFlight: Promise<void> | null = null
39
+
40
+ // One request at a time; entries queue meanwhile, bounded by MAX_QUEUE.
41
+ async function drain(): Promise<void> {
42
+ while (queue.length > 0 || dropped > 0) {
43
+ const entries = queue.splice(0, MAX_BATCH)
44
+ const droppedCount = dropped
45
+ dropped = 0
46
+ try {
47
+ await port.ingest({ entries, droppedCount })
48
+ } catch {
49
+ dropped += droppedCount + entries.length
50
+ return
51
+ }
52
+ }
53
+ }
54
+
55
+ function schedule(): void {
56
+ timer ??= window.setTimeout(() => void flush(), DELAY_MS)
57
+ }
58
+
59
+ function flush(): Promise<void> {
60
+ window.clearTimeout(timer)
61
+ timer = undefined
62
+ inFlight ??= drain().finally(() => {
63
+ inFlight = null
64
+ if (queue.length > 0) schedule()
65
+ })
66
+ return inFlight
67
+ }
68
+
69
+ function push(level: IngestLevel, event: string, fields?: LogFields): void {
70
+ if (!isValid(event, fields)) {
71
+ dropped++
72
+ return
73
+ }
74
+ if (queue.length >= MAX_QUEUE) {
75
+ // Full: drop the oldest debug/info first; a new debug/info entry is dropped itself.
76
+ dropped++
77
+ const low = queue.findIndex((entry) => entry.level === 'debug' || entry.level === 'info')
78
+ if (low >= 0) queue.splice(low, 1)
79
+ else if (level === 'debug' || level === 'info') return
80
+ else queue.shift()
81
+ }
82
+ queue.push(fields ? { level, event, fields } : { level, event })
83
+ if (queue.length >= BATCH) void flush()
84
+ else schedule()
85
+ }
86
+
87
+ window.addEventListener('pagehide', () => void flush())
88
+
89
+ return {
90
+ debug: (event, fields) => push('debug', event, fields),
91
+ info: (event, fields) => push('info', event, fields),
92
+ warn: (event, fields) => push('warn', event, fields),
93
+ error: (event, fields) => push('error', event, fields),
94
+ flush,
95
+ }
96
+ }
@@ -0,0 +1,8 @@
1
+ // Markdown → sanitized HTML. Render it with `{@attach (node) => { node.innerHTML = html }}`, never `{@html}` (docs/04 §5).
2
+ import DOMPurify from 'dompurify'
3
+ import { marked } from 'marked'
4
+
5
+ /** Model and skill text is untrusted: DOMPurify drops scripts, event handlers and `javascript:` URLs. */
6
+ export function renderMarkdown(source: string): string {
7
+ return DOMPurify.sanitize(marked.parse(source, { async: false, gfm: true }))
8
+ }
@@ -0,0 +1,204 @@
1
+ <script lang="ts">
2
+ import { Button } from '#lib/components/ui/button/index.js'
3
+ import * as Dialog from '#lib/components/ui/dialog/index.js'
4
+ import * as Field from '#lib/components/ui/field/index.js'
5
+ import { Input } from '#lib/components/ui/input/index.js'
6
+ import * as Select from '#lib/components/ui/select/index.js'
7
+ import { Switch } from '#lib/components/ui/switch/index.js'
8
+ import { Textarea } from '#lib/components/ui/textarea/index.js'
9
+ import CopyInput from '../CopyInput.svelte'
10
+ import { getKitContext } from '../context'
11
+ import type { McpTransport } from '../ports'
12
+
13
+ let {
14
+ open = $bindable(false),
15
+ editId,
16
+ name = $bindable(''),
17
+ slug = $bindable(''),
18
+ kind = $bindable<McpTransport['kind']>('http'),
19
+ url = $bindable(''),
20
+ command = $bindable(''),
21
+ args = $bindable(''),
22
+ cwd = $bindable(''),
23
+ replaceSecret = $bindable(true),
24
+ headers = $bindable(''),
25
+ query = $bindable(''),
26
+ env = $bindable(''),
27
+ currentKeys = [],
28
+ editorSaving = false,
29
+ busy = false,
30
+ onclose,
31
+ onsave,
32
+ onchangeKind,
33
+ onchangeReplacement,
34
+ oninputRevision,
35
+ onsecretInput,
36
+ }: {
37
+ open?: boolean
38
+ editId: string | null
39
+ name?: string
40
+ slug?: string
41
+ kind?: McpTransport['kind']
42
+ url?: string
43
+ command?: string
44
+ args?: string
45
+ cwd?: string
46
+ replaceSecret?: boolean
47
+ headers?: string
48
+ query?: string
49
+ env?: string
50
+ currentKeys?: string[]
51
+ editorSaving?: boolean
52
+ busy?: boolean
53
+ onclose: () => void
54
+ onsave: () => void
55
+ onchangeKind: (kind: McpTransport['kind']) => void
56
+ onchangeReplacement: (value: boolean) => void
57
+ oninputRevision: () => void
58
+ onsecretInput: () => void
59
+ } = $props()
60
+
61
+ const { t } = getKitContext().i18n
62
+ const KINDS: McpTransport['kind'][] = ['http', 'stdio']
63
+ const ARGS_PLACEHOLDER = '-y\n@modelcontextprotocol/server-filesystem'
64
+ </script>
65
+
66
+ <Dialog.Root
67
+ bind:open
68
+ onOpenChange={(isOpen) => {
69
+ if (!isOpen) onclose()
70
+ }}
71
+ >
72
+ <Dialog.Content class="sm:max-w-xl" oninput={oninputRevision}>
73
+ <Dialog.Header>
74
+ <Dialog.Title>{editId ? t('mcp.client.editTitle') : t('mcp.client.add')}</Dialog.Title>
75
+ </Dialog.Header>
76
+ <Field.Group>
77
+ <div class="flex gap-3">
78
+ <Field.Field class="flex-1">
79
+ <Field.Label for="kit-mcp-name">{t('mcp.client.name')}</Field.Label>
80
+ <Input id="kit-mcp-name" maxlength={200} bind:value={name} />
81
+ </Field.Field>
82
+ <Field.Field class="w-40">
83
+ <Field.Label for="kit-mcp-slug">{t('mcp.client.slug')}</Field.Label>
84
+ <Input id="kit-mcp-slug" class="font-mono" maxlength={24} bind:value={slug} />
85
+ </Field.Field>
86
+ </div>
87
+ <Field.Field>
88
+ <Field.Label for="kit-mcp-transport">{t('mcp.client.transport')}</Field.Label>
89
+ <Select.Root
90
+ type="single"
91
+ value={kind}
92
+ onValueChange={(value) => onchangeKind(value as McpTransport['kind'])}
93
+ >
94
+ <Select.Trigger id="kit-mcp-transport" class="w-full">
95
+ {t(`mcp.client.transport.${kind}`)}
96
+ </Select.Trigger>
97
+ <Select.Content>
98
+ <Select.Group>
99
+ {#each KINDS as option (option)}
100
+ <Select.Item value={option} label={t(`mcp.client.transport.${option}`)} />
101
+ {/each}
102
+ </Select.Group>
103
+ </Select.Content>
104
+ </Select.Root>
105
+ </Field.Field>
106
+ {#if kind === 'http'}
107
+ <Field.Field>
108
+ <Field.Label for="kit-mcp-url">{t('mcp.client.url')}</Field.Label>
109
+ <CopyInput
110
+ id="kit-mcp-url"
111
+ class="font-mono"
112
+ placeholder="https://example.com/mcp"
113
+ bind:value={url}
114
+ />
115
+ <Field.Description>{t('mcp.client.urlHint')}</Field.Description>
116
+ </Field.Field>
117
+ {:else}
118
+ <Field.Field>
119
+ <Field.Label for="kit-mcp-command">{t('mcp.client.command')}</Field.Label>
120
+ <Input
121
+ id="kit-mcp-command"
122
+ class="font-mono"
123
+ maxlength={1024}
124
+ placeholder="npx"
125
+ bind:value={command}
126
+ />
127
+ </Field.Field>
128
+ <Field.Field>
129
+ <Field.Label for="kit-mcp-args">{t('mcp.client.args')}</Field.Label>
130
+ <Textarea
131
+ id="kit-mcp-args"
132
+ class="font-mono text-xs"
133
+ placeholder={ARGS_PLACEHOLDER}
134
+ bind:value={args}
135
+ />
136
+ <Field.Description>{t('mcp.client.argsHint')}</Field.Description>
137
+ </Field.Field>
138
+ <Field.Field>
139
+ <Field.Label for="kit-mcp-cwd">{t('mcp.client.cwd')}</Field.Label>
140
+ <Input id="kit-mcp-cwd" class="font-mono" bind:value={cwd} />
141
+ <Field.Description>{t('mcp.client.cwdHint')}</Field.Description>
142
+ </Field.Field>
143
+ {/if}
144
+ {#if editId}
145
+ <Field.Field orientation="horizontal">
146
+ <Switch
147
+ id="kit-mcp-replace"
148
+ checked={replaceSecret}
149
+ onCheckedChange={onchangeReplacement}
150
+ />
151
+ <Field.Label for="kit-mcp-replace">
152
+ {t('mcp.client.replaceSecret', { keys: currentKeys.join(', ') || '—' })}
153
+ </Field.Label>
154
+ </Field.Field>
155
+ {/if}
156
+ {#if replaceSecret && kind === 'http'}
157
+ <Field.Field>
158
+ <Field.Label for="kit-mcp-headers">{t('mcp.client.headers')}</Field.Label>
159
+ <Textarea
160
+ id="kit-mcp-headers"
161
+ class="font-mono text-xs"
162
+ placeholder="Authorization: Bearer …"
163
+ bind:value={headers}
164
+ oninput={onsecretInput}
165
+ />
166
+ </Field.Field>
167
+ <Field.Field>
168
+ <Field.Label for="kit-mcp-query">{t('mcp.client.query')}</Field.Label>
169
+ <Textarea
170
+ id="kit-mcp-query"
171
+ class="font-mono text-xs"
172
+ placeholder="ak=…"
173
+ bind:value={query}
174
+ oninput={onsecretInput}
175
+ />
176
+ <Field.Description>{t('mcp.client.secretHint')}</Field.Description>
177
+ </Field.Field>
178
+ {:else if replaceSecret}
179
+ <Field.Field>
180
+ <Field.Label for="kit-mcp-env">{t('mcp.client.env')}</Field.Label>
181
+ <Textarea
182
+ id="kit-mcp-env"
183
+ class="font-mono text-xs"
184
+ placeholder="GITHUB_TOKEN=…"
185
+ bind:value={env}
186
+ oninput={onsecretInput}
187
+ />
188
+ <Field.Description>{t('mcp.client.envHint')}</Field.Description>
189
+ </Field.Field>
190
+ {/if}
191
+ </Field.Group>
192
+ <Dialog.Footer>
193
+ <Button variant="outline" onclick={onclose}>{t('common.cancel')}</Button>
194
+ <Button
195
+ disabled={busy ||
196
+ editorSaving ||
197
+ !name.trim() ||
198
+ !slug.trim() ||
199
+ !(kind === 'http' ? url.trim() : command.trim())}
200
+ onclick={onsave}>{t('mcp.client.save')}</Button
201
+ >
202
+ </Dialog.Footer>
203
+ </Dialog.Content>
204
+ </Dialog.Root>