@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,430 @@
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 {
10
+ KitClientError,
11
+ type ModelRef,
12
+ type PreparedSkill,
13
+ type RunEvent,
14
+ type SkillDto,
15
+ } from '../ports'
16
+ import ModelSelect from '../providers/ModelSelect.svelte'
17
+ import ApprovalCard from '../chat/ApprovalCard.svelte'
18
+ import ToolCallItem from '../chat/ToolCallItem.svelte'
19
+ import type { ToolItem } from '../chat/run-session.svelte'
20
+ import { errorText } from './error'
21
+ import SkillPreviewDialog from './SkillPreviewDialog.svelte'
22
+
23
+ let {
24
+ active,
25
+ open,
26
+ onclose,
27
+ onprepared,
28
+ }: {
29
+ active: boolean
30
+ open: boolean
31
+ onclose: () => void
32
+ onprepared: (prepared: PreparedSkill) => Promise<void>
33
+ } = $props()
34
+ const { ports, i18n, notify } = getKitContext()
35
+ const { t } = i18n
36
+ const authoring = ports.skillSources?.authoring
37
+ type Turn = {
38
+ id: number
39
+ input: string
40
+ text: string
41
+ tools: ToolItem[]
42
+ status: 'running' | 'completed' | 'failed' | 'cancelled'
43
+ }
44
+ let creator = $state<SkillDto | null>(null)
45
+ let session = $state<string | null>(null)
46
+ let model = $state<ModelRef | null>(null)
47
+ let input = $state('')
48
+ let creatorReview = $state(false)
49
+ let turns = $state<Turn[]>([])
50
+ let loading = $state(false)
51
+ let running = $state(false)
52
+ let error = $state<string | null>(null)
53
+ let runId = $state<string | null>(null)
54
+ let answering = $state<string[]>([])
55
+ let operation: AbortController | null = null
56
+ let epoch = 0
57
+ let alive = true
58
+ let nextTurn = 0
59
+ let cleanup: Promise<void> = Promise.resolve()
60
+
61
+ function report(cause: unknown): void {
62
+ notify.toast('error', errorText(t, cause))
63
+ }
64
+ async function release(id: string, run: string | null): Promise<void> {
65
+ if (!authoring) return
66
+ try {
67
+ if (run) await authoring.cancel(id, run)
68
+ } catch (cause) {
69
+ report(cause)
70
+ }
71
+ try {
72
+ await authoring.close(id)
73
+ } catch (cause) {
74
+ report(cause)
75
+ }
76
+ }
77
+ function reset(): void {
78
+ epoch += 1
79
+ operation?.abort()
80
+ operation = null
81
+ const previous = session
82
+ const run = runId
83
+ session = null
84
+ runId = null
85
+ running = false
86
+ loading = false
87
+ creator = null
88
+ turns = []
89
+ input = ''
90
+ error = null
91
+ answering = []
92
+ creatorReview = false
93
+ if (previous) cleanup = cleanup.then(() => release(previous, run))
94
+ }
95
+ async function loadCreator(): Promise<void> {
96
+ reset()
97
+ const currentEpoch = epoch
98
+ loading = true
99
+ try {
100
+ await cleanup
101
+ if (!alive || epoch !== currentEpoch || !active) return
102
+ const skills = await ports.skills?.list()
103
+ const builtin = skills?.find(
104
+ (skill) => skill.origin === 'builtin' && skill.name === 'skill-creator',
105
+ )
106
+ if (!builtin || !ports.skills)
107
+ throw new KitClientError({ code: 'needs_config', message: 'skill-creator unavailable' })
108
+ const full = await ports.skills.get(builtin.id)
109
+ if (alive && epoch === currentEpoch) creator = full
110
+ } catch (cause) {
111
+ if (alive && epoch === currentEpoch) error = errorText(t, cause)
112
+ } finally {
113
+ if (alive && epoch === currentEpoch) loading = false
114
+ }
115
+ }
116
+ $effect(() => {
117
+ if (active)
118
+ untrack(() => {
119
+ void loadCreator()
120
+ })
121
+ else untrack(reset)
122
+ })
123
+ onDestroy(() => {
124
+ alive = false
125
+ reset()
126
+ })
127
+
128
+ async function confirmCreator(): Promise<void> {
129
+ if (!authoring || !creator || loading || session) return
130
+ const currentEpoch = epoch
131
+ const controller = new AbortController()
132
+ operation = controller
133
+ loading = true
134
+ error = null
135
+ try {
136
+ const id = await authoring.create({ signal: controller.signal })
137
+ if (!alive || epoch !== currentEpoch || controller.signal.aborted) {
138
+ await release(id, null)
139
+ return
140
+ }
141
+ session = id
142
+ } catch (cause) {
143
+ if (alive && epoch === currentEpoch && !controller.signal.aborted) error = errorText(t, cause)
144
+ } finally {
145
+ if (alive && epoch === currentEpoch) {
146
+ loading = false
147
+ operation = null
148
+ }
149
+ }
150
+ }
151
+ function finish(turn: Turn, status: Turn['status']): void {
152
+ turn.status = status
153
+ for (const tool of turn.tools) tool.pending = null
154
+ running = false
155
+ runId = null
156
+ operation = null
157
+ answering = []
158
+ }
159
+ async function send(): Promise<void> {
160
+ if (!authoring || !session || running || loading || !input.trim()) return
161
+ if (!model) {
162
+ error = t('skills.add.choose_model')
163
+ return
164
+ }
165
+ const id = session
166
+ const currentEpoch = epoch
167
+ const controller = new AbortController()
168
+ operation = controller
169
+ const current = (): boolean =>
170
+ alive && epoch === currentEpoch && session === id && !controller.signal.aborted
171
+ const turnId = ++nextTurn
172
+ turns.push({ id: turnId, input: input.trim(), text: '', tools: [], status: 'running' })
173
+ // The pushed literal is wrapped by the $state proxy; mutate the proxied entry.
174
+ const turn = turns.at(-1)
175
+ if (!turn) return
176
+ input = ''
177
+ error = null
178
+ running = true
179
+ let ended = false
180
+ const receive = (event: RunEvent): void => {
181
+ if (!current() || ended) return
182
+ const tool = (callId: string): ToolItem | undefined =>
183
+ turn.tools.find((item) => item.callId === callId)
184
+ switch (event.kind) {
185
+ case 'text_delta':
186
+ turn.text += event.text
187
+ break
188
+ case 'tool_call':
189
+ if (!tool(event.callId))
190
+ turn.tools.push({
191
+ callId: event.callId,
192
+ name: event.name,
193
+ server: event.server ?? null,
194
+ pending: null,
195
+ outcome: null,
196
+ summary: '',
197
+ })
198
+ break
199
+ case 'approval_required': {
200
+ if (!tool(event.callId))
201
+ turn.tools.push({
202
+ callId: event.callId,
203
+ name: event.name,
204
+ server: null,
205
+ pending: null,
206
+ outcome: null,
207
+ summary: '',
208
+ })
209
+ const item = tool(event.callId)
210
+ if (item) item.pending = event.argsPreview
211
+ break
212
+ }
213
+ case 'tool_result': {
214
+ const item = tool(event.callId)
215
+ if (item)
216
+ Object.assign(item, { pending: null, outcome: event.outcome, summary: event.summary })
217
+ break
218
+ }
219
+ case 'end':
220
+ ended = true
221
+ finish(turn, event.status)
222
+ if (event.status === 'failed')
223
+ error = errorText(
224
+ t,
225
+ new KitClientError(event.error ?? { code: 'internal', message: '' }),
226
+ )
227
+ break
228
+ }
229
+ }
230
+ try {
231
+ const accepted = await authoring.send(id, turn.input, model, receive, {
232
+ signal: controller.signal,
233
+ })
234
+ if (!current()) {
235
+ if (!ended) await authoring.cancel(id, accepted.runId).catch(report)
236
+ if (alive && epoch === currentEpoch && session === id && controller.signal.aborted) {
237
+ finish(turn, 'cancelled')
238
+ loading = false
239
+ }
240
+ return
241
+ }
242
+ if (!ended) runId = accepted.runId
243
+ } catch (cause) {
244
+ if (alive && epoch === currentEpoch && session === id && !ended) {
245
+ finish(turn, controller.signal.aborted ? 'cancelled' : 'failed')
246
+ loading = false
247
+ if (!controller.signal.aborted) error = errorText(t, cause)
248
+ }
249
+ }
250
+ }
251
+ async function approve(tool: ToolItem, allow: boolean): Promise<void> {
252
+ if (
253
+ !authoring ||
254
+ !session ||
255
+ !runId ||
256
+ !running ||
257
+ tool.pending === null ||
258
+ answering.includes(tool.callId)
259
+ )
260
+ return
261
+ const id = session
262
+ const run = runId
263
+ const currentEpoch = epoch
264
+ answering = [...answering, tool.callId]
265
+ try {
266
+ await authoring.approve(id, run, tool.callId, allow)
267
+ if (alive && epoch === currentEpoch && runId === run) tool.pending = null
268
+ } catch (cause) {
269
+ if (alive && epoch === currentEpoch && runId === run) error = errorText(t, cause)
270
+ } finally {
271
+ if (alive && epoch === currentEpoch)
272
+ answering = answering.filter((callId) => callId !== tool.callId)
273
+ }
274
+ }
275
+ async function stop(): Promise<void> {
276
+ if (!authoring || !session || !running) return
277
+ const id = session
278
+ const currentEpoch = epoch
279
+ const run = runId
280
+ operation?.abort()
281
+ loading = true
282
+ if (!run) return
283
+ try {
284
+ await authoring.cancel(id, run)
285
+ } catch (cause) {
286
+ if (alive && epoch === currentEpoch) error = errorText(t, cause)
287
+ } finally {
288
+ if (alive && epoch === currentEpoch) {
289
+ const turn = turns.findLast((item) => item.status === 'running')
290
+ if (turn) finish(turn, 'cancelled')
291
+ loading = false
292
+ }
293
+ }
294
+ }
295
+ async function prepare(): Promise<void> {
296
+ if (!authoring || !session || running || loading) return
297
+ const id = session
298
+ const currentEpoch = epoch
299
+ const controller = new AbortController()
300
+ operation = controller
301
+ loading = true
302
+ error = null
303
+ try {
304
+ const prepared = await authoring.prepare(id, { signal: controller.signal })
305
+ if (alive && epoch === currentEpoch && session === id && !controller.signal.aborted)
306
+ await onprepared(prepared)
307
+ } catch (cause) {
308
+ if (alive && epoch === currentEpoch) error = errorText(t, cause)
309
+ } finally {
310
+ if (alive && epoch === currentEpoch) {
311
+ loading = false
312
+ operation = null
313
+ }
314
+ }
315
+ }
316
+ </script>
317
+
318
+ <Dialog.Root
319
+ open={open && !creatorReview}
320
+ onOpenChange={(value) => {
321
+ if (!value) onclose()
322
+ }}
323
+ >
324
+ <Dialog.Content class="flex max-h-[85vh] flex-col sm:max-w-2xl">
325
+ <Dialog.Header>
326
+ <Dialog.Title>{t('skills.add.agent')}</Dialog.Title>
327
+ <Dialog.Description>{t('skills.add.authoring_notice')}</Dialog.Description>
328
+ </Dialog.Header>
329
+ <div class="flex min-h-0 flex-col gap-4 overflow-y-auto">
330
+ {#if error}<p role="alert" class="text-destructive text-sm">{error}</p>{/if}
331
+ {#if !session}
332
+ <p class="text-sm">{t('skills.add.creator_review')}</p>
333
+ {#if creator}
334
+ <Button disabled={loading} onclick={() => (creatorReview = true)}
335
+ >{t('skills.add.review_creator')}</Button
336
+ >
337
+ {/if}
338
+ {:else}
339
+ <Field.Group>
340
+ <Field.Field>
341
+ <Field.Label>{t('skills.add.model')}</Field.Label>
342
+ <ModelSelect
343
+ value={model}
344
+ filter={{ modelType: 'chat' }}
345
+ onchange={(value) => (model = value)}
346
+ disabled={running || loading}
347
+ placeholder={t('skills.add.choose_model')}
348
+ />
349
+ </Field.Field>
350
+ <ol class="flex flex-col gap-4" aria-label={t('skills.add.conversation')}>
351
+ {#each turns as turn (turn.id)}
352
+ <li class="flex flex-col gap-2 rounded-md border p-3">
353
+ <p class="whitespace-pre-wrap text-sm">{turn.input}</p>
354
+ <p class="whitespace-pre-wrap text-sm">{turn.text}</p>
355
+ {#each turn.tools as tool (tool.callId)}
356
+ {#if tool.pending !== null}
357
+ <p class="text-destructive text-xs">{t('skills.add.tool_warning')}</p>
358
+ <fieldset disabled={!runId || answering.includes(tool.callId)}>
359
+ <ApprovalCard {tool} onAnswer={(allow) => void approve(tool, allow)} />
360
+ </fieldset>
361
+ {:else}<ToolCallItem {tool} />{/if}
362
+ {/each}
363
+ {#if turn.status === 'running'}<p
364
+ role="status"
365
+ class="text-muted-foreground text-xs"
366
+ >
367
+ {t('skills.add.working')}
368
+ </p>{/if}
369
+ {#if turn.status === 'cancelled'}<p
370
+ role="status"
371
+ class="text-muted-foreground text-xs"
372
+ >
373
+ {t('chat.stopped')}
374
+ </p>{/if}
375
+ </li>
376
+ {/each}
377
+ </ol>
378
+ <Field.Field>
379
+ <Field.Label for="skill-authoring-message"
380
+ >{t('skills.add.authoring_input')}</Field.Label
381
+ >
382
+ <Textarea
383
+ id="skill-authoring-message"
384
+ bind:value={input}
385
+ disabled={running || loading}
386
+ rows={3}
387
+ />
388
+ </Field.Field>
389
+ <div class="flex flex-wrap gap-2">
390
+ <Button disabled={running || loading || !input.trim()} onclick={() => void send()}
391
+ ><Bot data-icon="inline-start" />{t('chat.send')}</Button
392
+ >
393
+ {#if running}<Button variant="outline" onclick={() => void stop()}
394
+ >{t('chat.stop')}</Button
395
+ >{/if}
396
+ <Button
397
+ variant="outline"
398
+ disabled={running || loading || turns.length === 0}
399
+ onclick={() => void prepare()}>{t('skills.add.preview_artifact')}</Button
400
+ >
401
+ </div>
402
+ </Field.Group>
403
+ {/if}
404
+ {#if loading}<p role="status" class="text-muted-foreground text-sm">
405
+ {t('skills.add.working')}
406
+ </p>{/if}
407
+ </div>
408
+ <Dialog.Footer
409
+ ><Button variant="outline" onclick={onclose}>{t('common.cancel')}</Button></Dialog.Footer
410
+ >
411
+ </Dialog.Content>
412
+ </Dialog.Root>
413
+
414
+ <SkillPreviewDialog
415
+ skill={creator}
416
+ title={t('skills.add.creator_review')}
417
+ confirmLabel={t('skills.add.confirm_creator')}
418
+ busy={loading}
419
+ {error}
420
+ onconfirm={() => {
421
+ creatorReview = false
422
+ void confirmCreator()
423
+ }}
424
+ bind:open={
425
+ () => creatorReview && open,
426
+ (value) => {
427
+ if (!value) creatorReview = false
428
+ }
429
+ }
430
+ />
@@ -0,0 +1,104 @@
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
66
+ >{skill ? t('skills.edit_title', { name: skill.name }) : t('skills.new')}</Dialog.Title
67
+ >
68
+ </Dialog.Header>
69
+ <Field.Group class="min-h-0 overflow-y-auto">
70
+ <div class="flex gap-3">
71
+ <Field.Field class="flex-1">
72
+ <Field.Label for="kit-skill-name">{t('skills.name')}</Field.Label>
73
+ <Input id="kit-skill-name" class="font-mono" maxlength={64} bind:value={name} />
74
+ </Field.Field>
75
+ <Field.Field class="w-32">
76
+ <Field.Label for="kit-skill-version">{t('skills.version')}</Field.Label>
77
+ <Input
78
+ id="kit-skill-version"
79
+ maxlength={32}
80
+ placeholder={t('skills.unversioned')}
81
+ bind:value={version}
82
+ />
83
+ </Field.Field>
84
+ </div>
85
+ <Field.Field>
86
+ <Field.Label for="kit-skill-description">{t('skills.description')}</Field.Label>
87
+ <Input id="kit-skill-description" maxlength={500} bind:value={description} />
88
+ </Field.Field>
89
+ <Field.Field>
90
+ <Field.Label for="kit-skill-body">{t('skills.body')}</Field.Label>
91
+ <Textarea id="kit-skill-body" class="min-h-64 font-mono text-xs" bind:value={body} />
92
+ </Field.Field>
93
+ </Field.Group>
94
+ <Dialog.Footer>
95
+ <Button variant="outline" onclick={() => (open = false)}>{t('common.cancel')}</Button>
96
+ <Button
97
+ disabled={saving || !name.trim() || !description.trim() || !body.trim()}
98
+ onclick={() => void save()}
99
+ >
100
+ {t('skills.save')}
101
+ </Button>
102
+ </Dialog.Footer>
103
+ </Dialog.Content>
104
+ </Dialog.Root>
@@ -0,0 +1,136 @@
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<
12
+ Preview,
13
+ 'name' | 'description' | 'version' | 'body' | 'license' | 'extra' | 'files'
14
+ >
15
+
16
+ let {
17
+ open = $bindable(false),
18
+ skill,
19
+ title,
20
+ current = null,
21
+ confirmLabel,
22
+ busy = false,
23
+ error = null,
24
+ origin,
25
+ ignored = [],
26
+ onconfirm,
27
+ }: {
28
+ open: boolean
29
+ skill: SkillText | null
30
+ title: string
31
+ /** The installed skill a confirm would replace; confirming then needs the "replace" box. */
32
+ current?: PreviewCurrent | null
33
+ confirmLabel?: string
34
+ busy?: boolean
35
+ error?: string | null
36
+ origin?: string
37
+ ignored?: readonly string[]
38
+ onconfirm?: (replace: boolean) => void
39
+ } = $props()
40
+
41
+ const { t } = getKitContext().i18n
42
+ let replace = $state(false)
43
+ $effect.pre(() => {
44
+ void skill
45
+ if (open) replace = false
46
+ })
47
+ const extra = $derived(Object.entries(skill?.extra ?? {}))
48
+ const files = $derived(skill?.files ?? [])
49
+ const hasScripts = $derived(files.some((f) => f.kind === 'script'))
50
+ </script>
51
+
52
+ <Dialog.Root bind:open>
53
+ <Dialog.Content class="flex max-h-[85vh] flex-col sm:max-w-2xl">
54
+ <Dialog.Header>
55
+ <Dialog.Title>{title}</Dialog.Title>
56
+ <Dialog.Description>{t('skills.preview_desc')}</Dialog.Description>
57
+ </Dialog.Header>
58
+ {#if origin}<p class="text-muted-foreground break-all text-xs">{origin}</p>{/if}
59
+ {#if error}<p role="alert" class="text-destructive text-sm">{error}</p>{/if}
60
+ {#if skill}
61
+ <dl class="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm">
62
+ <dt class="text-muted-foreground">{t('skills.name')}</dt>
63
+ <dd class="font-mono">{skill.name}</dd>
64
+ <dt class="text-muted-foreground">{t('skills.version')}</dt>
65
+ <dd>{skill.version ?? t('skills.unversioned')}</dd>
66
+ <dt class="text-muted-foreground">{t('skills.description')}</dt>
67
+ <dd>{skill.description}</dd>
68
+ {#each extra as [key, value] (key)}
69
+ <dt class="text-muted-foreground font-mono">{key}</dt>
70
+ <dd>{value}</dd>
71
+ {/each}
72
+ </dl>
73
+ <pre
74
+ class="bg-muted min-h-0 flex-1 overflow-y-auto rounded-md p-3 text-xs whitespace-pre-wrap"
75
+ aria-label={t('skills.body')}>{skill.body}</pre>
76
+ {#if skill.license}
77
+ <details class="text-xs">
78
+ <summary class="cursor-pointer">{t('skills.license')}</summary>
79
+ <pre class="max-h-40 overflow-y-auto whitespace-pre-wrap">{skill.license}</pre>
80
+ </details>
81
+ {/if}
82
+ {#if hasScripts}
83
+ <Alert.Root>
84
+ <Alert.Description>{t('skills.scripts_notice')}</Alert.Description>
85
+ </Alert.Root>
86
+ {/if}
87
+ {#if files.length > 0}
88
+ <section
89
+ class="flex flex-col gap-1 text-xs"
90
+ aria-label={t('skills.files', { count: files.length })}
91
+ >
92
+ <h3 class="font-medium">{t('skills.files', { count: files.length })}</h3>
93
+ <ul class="max-h-40 overflow-y-auto font-mono">
94
+ {#each files as file (file.path)}
95
+ <li class="flex gap-3">
96
+ <span class="min-w-0 flex-1 truncate">{file.path}</span>
97
+ <span class="text-muted-foreground">{t(`skills.file_kind.${file.kind}`)}</span>
98
+ <span class="text-muted-foreground"
99
+ >{t('skills.file_size', { size: file.size })}</span
100
+ >
101
+ </li>
102
+ {/each}
103
+ </ul>
104
+ </section>
105
+ {/if}
106
+ {#if ignored.length > 0}
107
+ <section class="flex flex-col gap-1 text-xs" aria-label={t('skills.add.ignored')}>
108
+ <h3 class="font-medium">{t('skills.add.ignored')}</h3>
109
+ <ul class="max-h-32 overflow-y-auto break-all font-mono">
110
+ {#each ignored as path (path)}<li>{path}</li>{/each}
111
+ </ul>
112
+ </section>
113
+ {/if}
114
+ {#if onconfirm && current}
115
+ <label class="flex items-center gap-2 text-sm">
116
+ <input type="checkbox" class="accent-primary" bind:checked={replace} />
117
+ {t('skills.replace', {
118
+ version: current.version ?? t('skills.unversioned'),
119
+ revision: current.revision,
120
+ })}
121
+ </label>
122
+ {/if}
123
+ {/if}
124
+ <Dialog.Footer>
125
+ <Button variant="outline" onclick={() => (open = false)}>{t('common.cancel')}</Button>
126
+ {#if onconfirm}
127
+ <Button
128
+ disabled={busy || !skill || (current !== null && !replace)}
129
+ onclick={() => onconfirm(replace)}
130
+ >
131
+ {confirmLabel ?? t('skills.install')}
132
+ </Button>
133
+ {/if}
134
+ </Dialog.Footer>
135
+ </Dialog.Content>
136
+ </Dialog.Root>