@ouraihub/aether-ui 0.0.0-stage → 0.1.0

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 (179) 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 +23 -0
  28. package/src/chat/ChatPanel.svelte +2097 -0
  29. package/src/chat/MessageItem.svelte +583 -0
  30. package/src/chat/SkillPicker.svelte +126 -0
  31. package/src/chat/ToolCallItem.svelte +23 -0
  32. package/src/chat/UsageDetails.svelte +138 -0
  33. package/src/chat/index.ts +2 -0
  34. package/src/chat/run-session.svelte.ts +556 -0
  35. package/src/components/ui/alert/alert-action.svelte +20 -0
  36. package/src/components/ui/alert/alert-description.svelte +23 -0
  37. package/src/components/ui/alert/alert-title.svelte +23 -0
  38. package/src/components/ui/alert/alert.svelte +43 -0
  39. package/src/components/ui/alert/index.ts +17 -0
  40. package/src/components/ui/alert-dialog/alert-dialog-action.svelte +27 -0
  41. package/src/components/ui/alert-dialog/alert-dialog-cancel.svelte +27 -0
  42. package/src/components/ui/alert-dialog/alert-dialog-content.svelte +32 -0
  43. package/src/components/ui/alert-dialog/alert-dialog-description.svelte +17 -0
  44. package/src/components/ui/alert-dialog/alert-dialog-footer.svelte +23 -0
  45. package/src/components/ui/alert-dialog/alert-dialog-header.svelte +20 -0
  46. package/src/components/ui/alert-dialog/alert-dialog-media.svelte +20 -0
  47. package/src/components/ui/alert-dialog/alert-dialog-overlay.svelte +17 -0
  48. package/src/components/ui/alert-dialog/alert-dialog-portal.svelte +7 -0
  49. package/src/components/ui/alert-dialog/alert-dialog-title.svelte +17 -0
  50. package/src/components/ui/alert-dialog/alert-dialog-trigger.svelte +7 -0
  51. package/src/components/ui/alert-dialog/alert-dialog.svelte +7 -0
  52. package/src/components/ui/alert-dialog/index.ts +40 -0
  53. package/src/components/ui/button/button.svelte +81 -0
  54. package/src/components/ui/button/index.ts +17 -0
  55. package/src/components/ui/dialog/dialog-close.svelte +11 -0
  56. package/src/components/ui/dialog/dialog-content.svelte +48 -0
  57. package/src/components/ui/dialog/dialog-description.svelte +17 -0
  58. package/src/components/ui/dialog/dialog-footer.svelte +32 -0
  59. package/src/components/ui/dialog/dialog-header.svelte +20 -0
  60. package/src/components/ui/dialog/dialog-overlay.svelte +17 -0
  61. package/src/components/ui/dialog/dialog-portal.svelte +7 -0
  62. package/src/components/ui/dialog/dialog-title.svelte +17 -0
  63. package/src/components/ui/dialog/dialog-trigger.svelte +11 -0
  64. package/src/components/ui/dialog/dialog.svelte +7 -0
  65. package/src/components/ui/dialog/index.ts +34 -0
  66. package/src/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte +16 -0
  67. package/src/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +46 -0
  68. package/src/components/ui/dropdown-menu/dropdown-menu-content.svelte +31 -0
  69. package/src/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte +22 -0
  70. package/src/components/ui/dropdown-menu/dropdown-menu-group.svelte +7 -0
  71. package/src/components/ui/dropdown-menu/dropdown-menu-item.svelte +27 -0
  72. package/src/components/ui/dropdown-menu/dropdown-menu-label.svelte +24 -0
  73. package/src/components/ui/dropdown-menu/dropdown-menu-portal.svelte +7 -0
  74. package/src/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte +17 -0
  75. package/src/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte +36 -0
  76. package/src/components/ui/dropdown-menu/dropdown-menu-separator.svelte +17 -0
  77. package/src/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte +20 -0
  78. package/src/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte +31 -0
  79. package/src/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +29 -0
  80. package/src/components/ui/dropdown-menu/dropdown-menu-sub.svelte +7 -0
  81. package/src/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +7 -0
  82. package/src/components/ui/dropdown-menu/dropdown-menu.svelte +7 -0
  83. package/src/components/ui/dropdown-menu/index.ts +54 -0
  84. package/src/components/ui/empty/empty-content.svelte +23 -0
  85. package/src/components/ui/empty/empty-description.svelte +23 -0
  86. package/src/components/ui/empty/empty-header.svelte +20 -0
  87. package/src/components/ui/empty/empty-media.svelte +41 -0
  88. package/src/components/ui/empty/empty-title.svelte +20 -0
  89. package/src/components/ui/empty/empty.svelte +23 -0
  90. package/src/components/ui/empty/index.ts +22 -0
  91. package/src/components/ui/field/field-content.svelte +20 -0
  92. package/src/components/ui/field/field-description.svelte +25 -0
  93. package/src/components/ui/field/field-error.svelte +58 -0
  94. package/src/components/ui/field/field-group.svelte +23 -0
  95. package/src/components/ui/field/field-label.svelte +25 -0
  96. package/src/components/ui/field/field-legend.svelte +24 -0
  97. package/src/components/ui/field/field-separator.svelte +35 -0
  98. package/src/components/ui/field/field-set.svelte +20 -0
  99. package/src/components/ui/field/field-title.svelte +20 -0
  100. package/src/components/ui/field/field.svelte +47 -0
  101. package/src/components/ui/field/index.ts +33 -0
  102. package/src/components/ui/input/index.ts +7 -0
  103. package/src/components/ui/input/input.svelte +48 -0
  104. package/src/components/ui/label/index.ts +7 -0
  105. package/src/components/ui/label/label.svelte +20 -0
  106. package/src/components/ui/select/index.ts +40 -0
  107. package/src/components/ui/select/select-content.svelte +45 -0
  108. package/src/components/ui/select/select-group-heading.svelte +21 -0
  109. package/src/components/ui/select/select-group.svelte +17 -0
  110. package/src/components/ui/select/select-item.svelte +43 -0
  111. package/src/components/ui/select/select-label.svelte +20 -0
  112. package/src/components/ui/select/select-portal.svelte +7 -0
  113. package/src/components/ui/select/select-scroll-down-button.svelte +20 -0
  114. package/src/components/ui/select/select-scroll-up-button.svelte +20 -0
  115. package/src/components/ui/select/select-separator.svelte +18 -0
  116. package/src/components/ui/select/select-trigger.svelte +29 -0
  117. package/src/components/ui/select/select-value.svelte +7 -0
  118. package/src/components/ui/select/select.svelte +11 -0
  119. package/src/components/ui/separator/index.ts +7 -0
  120. package/src/components/ui/separator/separator.svelte +21 -0
  121. package/src/components/ui/sonner/index.ts +1 -0
  122. package/src/components/ui/sonner/sonner.svelte +40 -0
  123. package/src/components/ui/switch/index.ts +7 -0
  124. package/src/components/ui/switch/switch.svelte +34 -0
  125. package/src/components/ui/textarea/index.ts +7 -0
  126. package/src/components/ui/textarea/textarea.svelte +22 -0
  127. package/src/context.ts +34 -0
  128. package/src/dev/Demo.svelte +45 -0
  129. package/src/dev/main.ts +7 -0
  130. package/src/i18n/en.ts +617 -0
  131. package/src/i18n/index.ts +23 -0
  132. package/src/i18n/zh-CN.ts +599 -0
  133. package/src/index.ts +10 -0
  134. package/src/logs/LogPanel.svelte +97 -0
  135. package/src/logs/LogSettings.svelte +56 -0
  136. package/src/logs/index.ts +5 -0
  137. package/src/logs/logger.ts +96 -0
  138. package/src/markdown.ts +8 -0
  139. package/src/mcp/McpClientSettings.svelte +491 -0
  140. package/src/mcp/McpServerSettings.svelte +327 -0
  141. package/src/mcp/config.ts +26 -0
  142. package/src/mcp/index.ts +3 -0
  143. package/src/mcp/input.ts +73 -0
  144. package/src/notify.svelte.ts +43 -0
  145. package/src/ports/contract.ts +165 -0
  146. package/src/ports/fake.ts +484 -0
  147. package/src/ports/index.ts +7 -0
  148. package/src/ports/search.ts +47 -0
  149. package/src/ports/skill-sources.ts +45 -0
  150. package/src/providers/EndpointKeysDialog.svelte +343 -0
  151. package/src/providers/EndpointPanel.svelte +491 -0
  152. package/src/providers/ModelPickerDialog.svelte +187 -0
  153. package/src/providers/ModelSelect.svelte +434 -0
  154. package/src/providers/ModelSettingsDialog.svelte +453 -0
  155. package/src/providers/ProvidersSettings.svelte +631 -0
  156. package/src/providers/RequestConfigDialog.svelte +250 -0
  157. package/src/providers/SecretInput.svelte +276 -0
  158. package/src/providers/draft.ts +329 -0
  159. package/src/providers/edit-session.ts +146 -0
  160. package/src/providers/index.ts +5 -0
  161. package/src/providers/reasoning.ts +41 -0
  162. package/src/providers/revision.svelte.ts +4 -0
  163. package/src/search/SearchSettings.svelte +703 -0
  164. package/src/search/index.ts +13 -0
  165. package/src/search/settings-flow.ts +62 -0
  166. package/src/session.ts +20 -0
  167. package/src/skills/SkillAdd.svelte +347 -0
  168. package/src/skills/SkillAuthoring.svelte +381 -0
  169. package/src/skills/SkillEditor.svelte +94 -0
  170. package/src/skills/SkillPreviewDialog.svelte +122 -0
  171. package/src/skills/SkillsSettings.svelte +140 -0
  172. package/src/skills/error.ts +32 -0
  173. package/src/skills/index.ts +4 -0
  174. package/src/styles/kit.css +137 -0
  175. package/src/styles/tokens.css +22 -0
  176. package/src/tauri/index.ts +282 -0
  177. package/src/utils.ts +22 -0
  178. package/src/web/index.ts +376 -0
  179. package/README.md +0 -3
@@ -0,0 +1,381 @@
1
+ <script lang="ts">
2
+ import { onDestroy, untrack } from 'svelte'
3
+ import { Bot } from '@lucide/svelte'
4
+ import { Button } from '#lib/components/ui/button/index.js'
5
+ import * as Dialog from '#lib/components/ui/dialog/index.js'
6
+ import * as Field from '#lib/components/ui/field/index.js'
7
+ import { Textarea } from '#lib/components/ui/textarea/index.js'
8
+ import { getKitContext } from '../context'
9
+ import { KitClientError, type ModelRef, type PreparedSkill, type RunEvent, type SkillDto } from '../ports'
10
+ import ModelSelect from '../providers/ModelSelect.svelte'
11
+ import ApprovalCard from '../chat/ApprovalCard.svelte'
12
+ import ToolCallItem from '../chat/ToolCallItem.svelte'
13
+ import type { ToolItem } from '../chat/run-session.svelte'
14
+ import { errorText } from './error'
15
+ import SkillPreviewDialog from './SkillPreviewDialog.svelte'
16
+
17
+ let {
18
+ active,
19
+ open,
20
+ onclose,
21
+ onprepared,
22
+ }: {
23
+ active: boolean
24
+ open: boolean
25
+ onclose: () => void
26
+ onprepared: (prepared: PreparedSkill) => Promise<void>
27
+ } = $props()
28
+ const { ports, i18n, notify } = getKitContext()
29
+ const { t } = i18n
30
+ const authoring = ports.skillSources?.authoring
31
+ type Turn = {
32
+ id: number
33
+ input: string
34
+ text: string
35
+ tools: ToolItem[]
36
+ status: 'running' | 'completed' | 'failed' | 'cancelled'
37
+ }
38
+ let creator = $state<SkillDto | null>(null)
39
+ let session = $state<string | null>(null)
40
+ let model = $state<ModelRef | null>(null)
41
+ let input = $state('')
42
+ let creatorReview = $state(false)
43
+ let turns = $state<Turn[]>([])
44
+ let loading = $state(false)
45
+ let running = $state(false)
46
+ let error = $state<string | null>(null)
47
+ let runId = $state<string | null>(null)
48
+ let answering = $state<string[]>([])
49
+ let operation: AbortController | null = null
50
+ let epoch = 0
51
+ let alive = true
52
+ let nextTurn = 0
53
+ let cleanup: Promise<void> = Promise.resolve()
54
+
55
+ function report(cause: unknown): void {
56
+ notify.toast('error', errorText(t, cause))
57
+ }
58
+ async function release(id: string, run: string | null): Promise<void> {
59
+ if (!authoring) return
60
+ try {
61
+ if (run) await authoring.cancel(id, run)
62
+ } catch (cause) {
63
+ report(cause)
64
+ }
65
+ try {
66
+ await authoring.close(id)
67
+ } catch (cause) {
68
+ report(cause)
69
+ }
70
+ }
71
+ function reset(): void {
72
+ epoch += 1
73
+ operation?.abort()
74
+ operation = null
75
+ const previous = session
76
+ const run = runId
77
+ session = null
78
+ runId = null
79
+ running = false
80
+ loading = false
81
+ creator = null
82
+ turns = []
83
+ input = ''
84
+ error = null
85
+ answering = []
86
+ creatorReview = false
87
+ if (previous) cleanup = cleanup.then(() => release(previous, run))
88
+ }
89
+ async function loadCreator(): Promise<void> {
90
+ reset()
91
+ const currentEpoch = epoch
92
+ loading = true
93
+ try {
94
+ await cleanup
95
+ if (!alive || epoch !== currentEpoch || !active) return
96
+ const skills = await ports.skills?.list()
97
+ const builtin = skills?.find((skill) => skill.origin === 'builtin' && skill.name === 'skill-creator')
98
+ if (!builtin || !ports.skills)
99
+ throw new KitClientError({ code: 'needs_config', message: 'skill-creator unavailable' })
100
+ const full = await ports.skills.get(builtin.id)
101
+ if (alive && epoch === currentEpoch) creator = full
102
+ } catch (cause) {
103
+ if (alive && epoch === currentEpoch) error = errorText(t, cause)
104
+ } finally {
105
+ if (alive && epoch === currentEpoch) loading = false
106
+ }
107
+ }
108
+ $effect(() => {
109
+ if (active)
110
+ untrack(() => {
111
+ void loadCreator()
112
+ })
113
+ else untrack(reset)
114
+ })
115
+ onDestroy(() => {
116
+ alive = false
117
+ reset()
118
+ })
119
+
120
+ async function confirmCreator(): Promise<void> {
121
+ if (!authoring || !creator || loading || session) return
122
+ const currentEpoch = epoch
123
+ const controller = new AbortController()
124
+ operation = controller
125
+ loading = true
126
+ error = null
127
+ try {
128
+ const id = await authoring.create({ signal: controller.signal })
129
+ if (!alive || epoch !== currentEpoch || controller.signal.aborted) {
130
+ await release(id, null)
131
+ return
132
+ }
133
+ session = id
134
+ } catch (cause) {
135
+ if (alive && epoch === currentEpoch && !controller.signal.aborted) error = errorText(t, cause)
136
+ } finally {
137
+ if (alive && epoch === currentEpoch) {
138
+ loading = false
139
+ operation = null
140
+ }
141
+ }
142
+ }
143
+ function finish(turn: Turn, status: Turn['status']): void {
144
+ turn.status = status
145
+ for (const tool of turn.tools) tool.pending = null
146
+ running = false
147
+ runId = null
148
+ operation = null
149
+ answering = []
150
+ }
151
+ async function send(): Promise<void> {
152
+ if (!authoring || !session || running || loading || !input.trim()) return
153
+ if (!model) {
154
+ error = t('skills.add.choose_model')
155
+ return
156
+ }
157
+ const id = session
158
+ const currentEpoch = epoch
159
+ const controller = new AbortController()
160
+ operation = controller
161
+ const current = (): boolean => alive && epoch === currentEpoch && session === id && !controller.signal.aborted
162
+ const turnId = ++nextTurn
163
+ turns.push({ id: turnId, input: input.trim(), text: '', tools: [], status: 'running' })
164
+ // The pushed literal is wrapped by the $state proxy; mutate the proxied entry.
165
+ const turn = turns.at(-1)
166
+ if (!turn) return
167
+ input = ''
168
+ error = null
169
+ running = true
170
+ let ended = false
171
+ const receive = (event: RunEvent): void => {
172
+ if (!current() || ended) return
173
+ const tool = (callId: string): ToolItem | undefined => turn.tools.find((item) => item.callId === callId)
174
+ switch (event.kind) {
175
+ case 'text_delta':
176
+ turn.text += event.text
177
+ break
178
+ case 'tool_call':
179
+ if (!tool(event.callId))
180
+ turn.tools.push({
181
+ callId: event.callId,
182
+ name: event.name,
183
+ server: event.server ?? null,
184
+ pending: null,
185
+ outcome: null,
186
+ summary: '',
187
+ })
188
+ break
189
+ case 'approval_required': {
190
+ if (!tool(event.callId))
191
+ turn.tools.push({
192
+ callId: event.callId,
193
+ name: event.name,
194
+ server: null,
195
+ pending: null,
196
+ outcome: null,
197
+ summary: '',
198
+ })
199
+ const item = tool(event.callId)
200
+ if (item) item.pending = event.argsPreview
201
+ break
202
+ }
203
+ case 'tool_result': {
204
+ const item = tool(event.callId)
205
+ if (item) Object.assign(item, { pending: null, outcome: event.outcome, summary: event.summary })
206
+ break
207
+ }
208
+ case 'end':
209
+ ended = true
210
+ finish(turn, event.status)
211
+ if (event.status === 'failed')
212
+ error = errorText(t, new KitClientError(event.error ?? { code: 'internal', message: '' }))
213
+ break
214
+ }
215
+ }
216
+ try {
217
+ const accepted = await authoring.send(id, turn.input, model, receive, { signal: controller.signal })
218
+ if (!current()) {
219
+ if (!ended) await authoring.cancel(id, accepted.runId).catch(report)
220
+ if (alive && epoch === currentEpoch && session === id && controller.signal.aborted) {
221
+ finish(turn, 'cancelled')
222
+ loading = false
223
+ }
224
+ return
225
+ }
226
+ if (!ended) runId = accepted.runId
227
+ } catch (cause) {
228
+ if (alive && epoch === currentEpoch && session === id && !ended) {
229
+ finish(turn, controller.signal.aborted ? 'cancelled' : 'failed')
230
+ loading = false
231
+ if (!controller.signal.aborted) error = errorText(t, cause)
232
+ }
233
+ }
234
+ }
235
+ async function approve(tool: ToolItem, allow: boolean): Promise<void> {
236
+ if (!authoring || !session || !runId || !running || tool.pending === null || answering.includes(tool.callId)) return
237
+ const id = session
238
+ const run = runId
239
+ const currentEpoch = epoch
240
+ answering = [...answering, tool.callId]
241
+ try {
242
+ await authoring.approve(id, run, tool.callId, allow)
243
+ if (alive && epoch === currentEpoch && runId === run) tool.pending = null
244
+ } catch (cause) {
245
+ if (alive && epoch === currentEpoch && runId === run) error = errorText(t, cause)
246
+ } finally {
247
+ if (alive && epoch === currentEpoch) answering = answering.filter((callId) => callId !== tool.callId)
248
+ }
249
+ }
250
+ async function stop(): Promise<void> {
251
+ if (!authoring || !session || !running) return
252
+ const id = session
253
+ const currentEpoch = epoch
254
+ const run = runId
255
+ operation?.abort()
256
+ loading = true
257
+ if (!run) return
258
+ try {
259
+ await authoring.cancel(id, run)
260
+ } catch (cause) {
261
+ if (alive && epoch === currentEpoch) error = errorText(t, cause)
262
+ } finally {
263
+ if (alive && epoch === currentEpoch) {
264
+ const turn = turns.findLast((item) => item.status === 'running')
265
+ if (turn) finish(turn, 'cancelled')
266
+ loading = false
267
+ }
268
+ }
269
+ }
270
+ async function prepare(): Promise<void> {
271
+ if (!authoring || !session || running || loading) return
272
+ const id = session
273
+ const currentEpoch = epoch
274
+ const controller = new AbortController()
275
+ operation = controller
276
+ loading = true
277
+ error = null
278
+ try {
279
+ const prepared = await authoring.prepare(id, { signal: controller.signal })
280
+ if (alive && epoch === currentEpoch && session === id && !controller.signal.aborted) await onprepared(prepared)
281
+ } catch (cause) {
282
+ if (alive && epoch === currentEpoch) error = errorText(t, cause)
283
+ } finally {
284
+ if (alive && epoch === currentEpoch) {
285
+ loading = false
286
+ operation = null
287
+ }
288
+ }
289
+ }
290
+ </script>
291
+
292
+ <Dialog.Root
293
+ open={open && !creatorReview}
294
+ onOpenChange={(value) => {
295
+ if (!value) onclose()
296
+ }}
297
+ >
298
+ <Dialog.Content class="flex max-h-[85vh] flex-col sm:max-w-2xl">
299
+ <Dialog.Header>
300
+ <Dialog.Title>{t('skills.add.agent')}</Dialog.Title>
301
+ <Dialog.Description>{t('skills.add.authoring_notice')}</Dialog.Description>
302
+ </Dialog.Header>
303
+ <div class="flex min-h-0 flex-col gap-4 overflow-y-auto">
304
+ {#if error}<p role="alert" class="text-destructive text-sm">{error}</p>{/if}
305
+ {#if !session}
306
+ <p class="text-sm">{t('skills.add.creator_review')}</p>
307
+ {#if creator}
308
+ <Button disabled={loading} onclick={() => (creatorReview = true)}>{t('skills.add.review_creator')}</Button>
309
+ {/if}
310
+ {:else}
311
+ <Field.Group>
312
+ <Field.Field>
313
+ <Field.Label>{t('skills.add.model')}</Field.Label>
314
+ <ModelSelect
315
+ value={model}
316
+ filter={{ modelType: 'chat' }}
317
+ onchange={(value) => (model = value)}
318
+ disabled={running || loading}
319
+ placeholder={t('skills.add.choose_model')}
320
+ />
321
+ </Field.Field>
322
+ <ol class="flex flex-col gap-4" aria-label={t('skills.add.conversation')}>
323
+ {#each turns as turn (turn.id)}
324
+ <li class="flex flex-col gap-2 rounded-md border p-3">
325
+ <p class="whitespace-pre-wrap text-sm">{turn.input}</p>
326
+ <p class="whitespace-pre-wrap text-sm">{turn.text}</p>
327
+ {#each turn.tools as tool (tool.callId)}
328
+ {#if tool.pending !== null}
329
+ <p class="text-destructive text-xs">{t('skills.add.tool_warning')}</p>
330
+ <fieldset disabled={!runId || answering.includes(tool.callId)}>
331
+ <ApprovalCard {tool} onAnswer={(allow) => void approve(tool, allow)} />
332
+ </fieldset>
333
+ {:else}<ToolCallItem {tool} />{/if}
334
+ {/each}
335
+ {#if turn.status === 'running'}<p role="status" class="text-muted-foreground text-xs">
336
+ {t('skills.add.working')}
337
+ </p>{/if}
338
+ {#if turn.status === 'cancelled'}<p role="status" class="text-muted-foreground text-xs">
339
+ {t('chat.stopped')}
340
+ </p>{/if}
341
+ </li>
342
+ {/each}
343
+ </ol>
344
+ <Field.Field>
345
+ <Field.Label for="skill-authoring-message">{t('skills.add.authoring_input')}</Field.Label>
346
+ <Textarea id="skill-authoring-message" bind:value={input} disabled={running || loading} rows={3} />
347
+ </Field.Field>
348
+ <div class="flex flex-wrap gap-2">
349
+ <Button disabled={running || loading || !input.trim()} onclick={() => void send()}
350
+ ><Bot data-icon="inline-start" />{t('chat.send')}</Button
351
+ >
352
+ {#if running}<Button variant="outline" onclick={() => void stop()}>{t('chat.stop')}</Button>{/if}
353
+ <Button variant="outline" disabled={running || loading || turns.length === 0} onclick={() => void prepare()}
354
+ >{t('skills.add.preview_artifact')}</Button
355
+ >
356
+ </div>
357
+ </Field.Group>
358
+ {/if}
359
+ {#if loading}<p role="status" class="text-muted-foreground text-sm">{t('skills.add.working')}</p>{/if}
360
+ </div>
361
+ <Dialog.Footer><Button variant="outline" onclick={onclose}>{t('common.cancel')}</Button></Dialog.Footer>
362
+ </Dialog.Content>
363
+ </Dialog.Root>
364
+
365
+ <SkillPreviewDialog
366
+ skill={creator}
367
+ title={t('skills.add.creator_review')}
368
+ confirmLabel={t('skills.add.confirm_creator')}
369
+ busy={loading}
370
+ {error}
371
+ onconfirm={() => {
372
+ creatorReview = false
373
+ void confirmCreator()
374
+ }}
375
+ bind:open={
376
+ () => creatorReview && open,
377
+ (value) => {
378
+ if (!value) creatorReview = false
379
+ }
380
+ }
381
+ />
@@ -0,0 +1,94 @@
1
+ <script lang="ts">
2
+ // Create or edit a hand-written skill (docs/14 §6). Editing sends `expectedRevision`, so a
3
+ // concurrent edit elsewhere fails with `conflict{revision_changed}` instead of being overwritten.
4
+ import { Button } from '#lib/components/ui/button/index.js'
5
+ import * as Dialog from '#lib/components/ui/dialog/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 { Textarea } from '#lib/components/ui/textarea/index.js'
9
+ import { getKitContext } from '../context'
10
+ import type { ManualSave, SkillDto, SkillMeta, SkillsPort } from '../ports'
11
+ import { errorText } from './error'
12
+
13
+ let {
14
+ open = $bindable(false),
15
+ port,
16
+ skill,
17
+ onsaved,
18
+ }: {
19
+ open: boolean
20
+ port: SkillsPort
21
+ /** The skill being edited; null creates a new one. */
22
+ skill: SkillDto | null
23
+ onsaved: (skills: SkillMeta[]) => void
24
+ } = $props()
25
+
26
+ const { i18n, notify } = getKitContext()
27
+ const { t } = i18n
28
+ let name = $state('')
29
+ let description = $state('')
30
+ let version = $state('')
31
+ let body = $state('')
32
+ let saving = $state(false)
33
+
34
+ $effect.pre(() => {
35
+ if (!open) return
36
+ name = skill?.name ?? ''
37
+ description = skill?.description ?? ''
38
+ version = skill?.version ?? ''
39
+ body = skill?.body ?? ''
40
+ })
41
+
42
+ async function save() {
43
+ const input: ManualSave = {
44
+ name: name.trim(),
45
+ description: description.trim(),
46
+ version: version.trim() || null,
47
+ body,
48
+ }
49
+ if (skill) Object.assign(input, { id: skill.id, expectedRevision: skill.revision })
50
+ saving = true
51
+ try {
52
+ onsaved(await port.save(input))
53
+ open = false
54
+ } catch (error) {
55
+ notify.toast('error', errorText(t, error))
56
+ } finally {
57
+ saving = false
58
+ }
59
+ }
60
+ </script>
61
+
62
+ <Dialog.Root bind:open>
63
+ <Dialog.Content class="flex max-h-[85vh] flex-col sm:max-w-2xl">
64
+ <Dialog.Header>
65
+ <Dialog.Title>{skill ? t('skills.edit_title', { name: skill.name }) : t('skills.new')}</Dialog.Title>
66
+ </Dialog.Header>
67
+ <Field.Group class="min-h-0 overflow-y-auto">
68
+ <div class="flex gap-3">
69
+ <Field.Field class="flex-1">
70
+ <Field.Label for="kit-skill-name">{t('skills.name')}</Field.Label>
71
+ <Input id="kit-skill-name" class="font-mono" maxlength={64} bind:value={name} />
72
+ </Field.Field>
73
+ <Field.Field class="w-32">
74
+ <Field.Label for="kit-skill-version">{t('skills.version')}</Field.Label>
75
+ <Input id="kit-skill-version" maxlength={32} placeholder={t('skills.unversioned')} bind:value={version} />
76
+ </Field.Field>
77
+ </div>
78
+ <Field.Field>
79
+ <Field.Label for="kit-skill-description">{t('skills.description')}</Field.Label>
80
+ <Input id="kit-skill-description" maxlength={500} bind:value={description} />
81
+ </Field.Field>
82
+ <Field.Field>
83
+ <Field.Label for="kit-skill-body">{t('skills.body')}</Field.Label>
84
+ <Textarea id="kit-skill-body" class="min-h-64 font-mono text-xs" bind:value={body} />
85
+ </Field.Field>
86
+ </Field.Group>
87
+ <Dialog.Footer>
88
+ <Button variant="outline" onclick={() => (open = false)}>{t('common.cancel')}</Button>
89
+ <Button disabled={saving || !name.trim() || !description.trim() || !body.trim()} onclick={() => void save()}>
90
+ {t('skills.save')}
91
+ </Button>
92
+ </Dialog.Footer>
93
+ </Dialog.Content>
94
+ </Dialog.Root>
@@ -0,0 +1,122 @@
1
+ <script lang="ts">
2
+ // Full text and packaged files of a skill before it is installed or used (docs/16 §4.3, docs/14 §4).
3
+ // Without `onconfirm` it is a read-only viewer.
4
+ import * as Alert from '#lib/components/ui/alert/index.js'
5
+ import { Button } from '#lib/components/ui/button/index.js'
6
+ import * as Dialog from '#lib/components/ui/dialog/index.js'
7
+ import { getKitContext } from '../context'
8
+ import type { Preview, PreviewCurrent } from '../ports'
9
+
10
+ /** What both a Preview and a stored SkillDto carry. */
11
+ type SkillText = Pick<Preview, 'name' | 'description' | 'version' | 'body' | 'license' | 'extra' | 'files'>
12
+
13
+ let {
14
+ open = $bindable(false),
15
+ skill,
16
+ title,
17
+ current = null,
18
+ confirmLabel,
19
+ busy = false,
20
+ error = null,
21
+ origin,
22
+ ignored = [],
23
+ onconfirm,
24
+ }: {
25
+ open: boolean
26
+ skill: SkillText | null
27
+ title: string
28
+ /** The installed skill a confirm would replace; confirming then needs the "replace" box. */
29
+ current?: PreviewCurrent | null
30
+ confirmLabel?: string
31
+ busy?: boolean
32
+ error?: string | null
33
+ origin?: string
34
+ ignored?: readonly string[]
35
+ onconfirm?: (replace: boolean) => void
36
+ } = $props()
37
+
38
+ const { t } = getKitContext().i18n
39
+ let replace = $state(false)
40
+ $effect.pre(() => {
41
+ void skill
42
+ if (open) replace = false
43
+ })
44
+ const extra = $derived(Object.entries(skill?.extra ?? {}))
45
+ const files = $derived(skill?.files ?? [])
46
+ const hasScripts = $derived(files.some((f) => f.kind === 'script'))
47
+ </script>
48
+
49
+ <Dialog.Root bind:open>
50
+ <Dialog.Content class="flex max-h-[85vh] flex-col sm:max-w-2xl">
51
+ <Dialog.Header>
52
+ <Dialog.Title>{title}</Dialog.Title>
53
+ <Dialog.Description>{t('skills.preview_desc')}</Dialog.Description>
54
+ </Dialog.Header>
55
+ {#if origin}<p class="text-muted-foreground break-all text-xs">{origin}</p>{/if}
56
+ {#if error}<p role="alert" class="text-destructive text-sm">{error}</p>{/if}
57
+ {#if skill}
58
+ <dl class="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm">
59
+ <dt class="text-muted-foreground">{t('skills.name')}</dt>
60
+ <dd class="font-mono">{skill.name}</dd>
61
+ <dt class="text-muted-foreground">{t('skills.version')}</dt>
62
+ <dd>{skill.version ?? t('skills.unversioned')}</dd>
63
+ <dt class="text-muted-foreground">{t('skills.description')}</dt>
64
+ <dd>{skill.description}</dd>
65
+ {#each extra as [key, value] (key)}
66
+ <dt class="text-muted-foreground font-mono">{key}</dt>
67
+ <dd>{value}</dd>
68
+ {/each}
69
+ </dl>
70
+ <pre
71
+ class="bg-muted min-h-0 flex-1 overflow-y-auto rounded-md p-3 text-xs whitespace-pre-wrap"
72
+ aria-label={t('skills.body')}>{skill.body}</pre>
73
+ {#if skill.license}
74
+ <details class="text-xs">
75
+ <summary class="cursor-pointer">{t('skills.license')}</summary>
76
+ <pre class="max-h-40 overflow-y-auto whitespace-pre-wrap">{skill.license}</pre>
77
+ </details>
78
+ {/if}
79
+ {#if hasScripts}
80
+ <Alert.Root>
81
+ <Alert.Description>{t('skills.scripts_notice')}</Alert.Description>
82
+ </Alert.Root>
83
+ {/if}
84
+ {#if files.length > 0}
85
+ <section class="flex flex-col gap-1 text-xs" aria-label={t('skills.files', { count: files.length })}>
86
+ <h3 class="font-medium">{t('skills.files', { count: files.length })}</h3>
87
+ <ul class="max-h-40 overflow-y-auto font-mono">
88
+ {#each files as file (file.path)}
89
+ <li class="flex gap-3">
90
+ <span class="min-w-0 flex-1 truncate">{file.path}</span>
91
+ <span class="text-muted-foreground">{t(`skills.file_kind.${file.kind}`)}</span>
92
+ <span class="text-muted-foreground">{t('skills.file_size', { size: file.size })}</span>
93
+ </li>
94
+ {/each}
95
+ </ul>
96
+ </section>
97
+ {/if}
98
+ {#if ignored.length > 0}
99
+ <section class="flex flex-col gap-1 text-xs" aria-label={t('skills.add.ignored')}>
100
+ <h3 class="font-medium">{t('skills.add.ignored')}</h3>
101
+ <ul class="max-h-32 overflow-y-auto break-all font-mono">
102
+ {#each ignored as path (path)}<li>{path}</li>{/each}
103
+ </ul>
104
+ </section>
105
+ {/if}
106
+ {#if onconfirm && current}
107
+ <label class="flex items-center gap-2 text-sm">
108
+ <input type="checkbox" class="accent-primary" bind:checked={replace} />
109
+ {t('skills.replace', { version: current.version ?? t('skills.unversioned'), revision: current.revision })}
110
+ </label>
111
+ {/if}
112
+ {/if}
113
+ <Dialog.Footer>
114
+ <Button variant="outline" onclick={() => (open = false)}>{t('common.cancel')}</Button>
115
+ {#if onconfirm}
116
+ <Button disabled={busy || !skill || (current !== null && !replace)} onclick={() => onconfirm(replace)}>
117
+ {confirmLabel ?? t('skills.install')}
118
+ </Button>
119
+ {/if}
120
+ </Dialog.Footer>
121
+ </Dialog.Content>
122
+ </Dialog.Root>