@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,380 @@
1
+ <script lang="ts">
2
+ import { onDestroy } from 'svelte'
3
+ import { Bot, ChevronDown, FolderSearch, Import, Plus, Search } 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 DropdownMenu from '#lib/components/ui/dropdown-menu/index.js'
7
+ import * as Field from '#lib/components/ui/field/index.js'
8
+ import { Input } from '#lib/components/ui/input/index.js'
9
+ import { getKitContext } from '../context'
10
+ import {
11
+ isKitClientError,
12
+ type Preview,
13
+ type PreparedSkill,
14
+ type SkillMeta,
15
+ type Source,
16
+ type SkillCandidate,
17
+ type SystemSkillCandidate,
18
+ } from '../ports'
19
+ import { errorText } from './error'
20
+ import SkillPreviewDialog from './SkillPreviewDialog.svelte'
21
+ import SkillAuthoring from './SkillAuthoring.svelte'
22
+
23
+ let {
24
+ oninstalled,
25
+ onmanual,
26
+ disabled = false,
27
+ }: {
28
+ oninstalled: (skills: SkillMeta[]) => void
29
+ onmanual: () => void
30
+ disabled?: boolean
31
+ } = $props()
32
+ const { ports, i18n, notify } = getKitContext()
33
+ const { t } = i18n
34
+ const port = ports.skills
35
+ const sources = ports.skillSources
36
+ type Mode = 'agent' | 'online' | 'system' | 'local'
37
+ let mode = $state<Mode | null>(null)
38
+ let busy = $state(false)
39
+ let error = $state<string | null>(null)
40
+ let query = $state('')
41
+ let candidates = $state<SkillCandidate[]>([])
42
+ let system = $state<SystemSkillCandidate[]>([])
43
+ let searched = $state(false)
44
+ let file = $state<HTMLInputElement | null>(null)
45
+ let source: Source | null = null
46
+ let preview = $state<Preview | null>(null)
47
+ let origin = $state('')
48
+ let ignored = $state<string[]>([])
49
+ let operation: AbortController | null = null
50
+ let generation = 0
51
+ let alive = true
52
+
53
+ function invalidate(): void {
54
+ generation += 1
55
+ operation?.abort()
56
+ operation = null
57
+ busy = false
58
+ }
59
+ function close(): void {
60
+ invalidate()
61
+ mode = null
62
+ preview = null
63
+ source = null
64
+ error = null
65
+ }
66
+ onDestroy(() => {
67
+ alive = false
68
+ invalidate()
69
+ })
70
+
71
+ async function act(
72
+ work: (signal: AbortSignal, current: () => boolean) => Promise<void>,
73
+ ): Promise<void> {
74
+ invalidate()
75
+ const id = generation
76
+ const controller = new AbortController()
77
+ operation = controller
78
+ const current = (): boolean => alive && generation === id && !controller.signal.aborted
79
+ busy = true
80
+ error = null
81
+ try {
82
+ await work(controller.signal, current)
83
+ } catch (cause) {
84
+ if (current()) error = errorText(t, cause)
85
+ } finally {
86
+ if (current()) {
87
+ busy = false
88
+ operation = null
89
+ }
90
+ }
91
+ }
92
+ function open(next: Mode): void {
93
+ close()
94
+ query = ''
95
+ candidates = []
96
+ system = []
97
+ searched = false
98
+ mode = next
99
+ if (next === 'system') void scan(false)
100
+ }
101
+ async function showPreview(
102
+ next: Source,
103
+ from: string,
104
+ current: () => boolean,
105
+ excluded: string[] = [],
106
+ ): Promise<void> {
107
+ if (!port || !current()) return
108
+ const result = await port.preview(next)
109
+ if (!current()) return
110
+ source = next
111
+ origin = from
112
+ ignored = excluded
113
+ preview = result
114
+ }
115
+ function search(): Promise<void> {
116
+ return act(async (signal, current) => {
117
+ if (!sources) return
118
+ const result = await sources.search(query.trim(), { signal })
119
+ if (current()) {
120
+ candidates = result
121
+ searched = true
122
+ }
123
+ })
124
+ }
125
+ function prepare(candidate: SkillCandidate): Promise<void> {
126
+ return act(async (signal, current) => {
127
+ if (!sources) return
128
+ const prepared = await sources.prepare(candidate.url, { signal })
129
+ await showPreview(prepared.source, prepared.origin, current, prepared.ignored)
130
+ })
131
+ }
132
+ function scan(pick: boolean): Promise<void> {
133
+ return act(async (signal, current) => {
134
+ if (!sources) return
135
+ const found = await (pick
136
+ ? sources.pickSearchDirectory({ signal })
137
+ : sources.discover({ signal }))
138
+ if (current() && found !== null) {
139
+ system = found
140
+ searched = true
141
+ }
142
+ })
143
+ }
144
+ function prepareSystem(candidate: SystemSkillCandidate): Promise<void> {
145
+ return act(async (signal, current) => {
146
+ if (!sources) return
147
+ const prepared = await sources.prepareSystem(candidate.path, { signal })
148
+ await showPreview(prepared.source, prepared.origin, current, prepared.ignored)
149
+ })
150
+ }
151
+ function pickDirectory(): Promise<void> {
152
+ return act(async (signal, current) => {
153
+ const prepared = await sources?.pickDirectory({ signal })
154
+ if (prepared) await showPreview(prepared.source, prepared.origin, current, prepared.ignored)
155
+ })
156
+ }
157
+ function pickFile(input: HTMLInputElement): void {
158
+ const picked = input.files?.[0]
159
+ input.value = ''
160
+ if (!picked) return
161
+ void act(async (_signal, current) => {
162
+ const next: Source = {
163
+ bytes: { fileName: picked.name, bytes: [...new Uint8Array(await picked.arrayBuffer())] },
164
+ }
165
+ await showPreview(next, picked.name, current)
166
+ })
167
+ }
168
+ function closePreview(): void {
169
+ invalidate()
170
+ preview = null
171
+ source = null
172
+ }
173
+ function install(replace: boolean): void {
174
+ const confirmed = preview
175
+ const from = source
176
+ if (!port || !confirmed || !from || busy) return
177
+ void act(async (_signal, current) => {
178
+ try {
179
+ const installed = await port.install(from, confirmed.digest, replace)
180
+ if (!current()) return
181
+ oninstalled(installed)
182
+ close()
183
+ notify.toast('success', t('skills.installed', { name: confirmed.name }))
184
+ } catch (cause) {
185
+ if (isKitClientError(cause) && cause.details.reason === 'content_changed') {
186
+ const refreshed = await port.preview(from)
187
+ if (current()) {
188
+ preview = refreshed
189
+ notify.toast('info', t('skills.content_changed'))
190
+ }
191
+ return
192
+ }
193
+ throw cause
194
+ }
195
+ })
196
+ }
197
+ </script>
198
+
199
+ <div class="flex gap-2">
200
+ <DropdownMenu.Root>
201
+ <DropdownMenu.Trigger>
202
+ {#snippet child({ props })}
203
+ <Button {...props} {disabled}
204
+ ><Plus data-icon="inline-start" />{t('skills.add.title')}<ChevronDown
205
+ data-icon="inline-end"
206
+ /></Button
207
+ >
208
+ {/snippet}
209
+ </DropdownMenu.Trigger>
210
+ <DropdownMenu.Content align="start">
211
+ <DropdownMenu.Group>
212
+ {#if sources}
213
+ {#if sources.authoring && ports.providers}
214
+ <DropdownMenu.Item onSelect={() => open('agent')}
215
+ ><Bot />{t('skills.add.agent')}</DropdownMenu.Item
216
+ >
217
+ {/if}
218
+ <DropdownMenu.Item onSelect={() => open('online')}
219
+ ><Search />{t('skills.add.online')}</DropdownMenu.Item
220
+ >
221
+ <DropdownMenu.Item onSelect={() => open('system')}
222
+ ><FolderSearch />{t('skills.add.system')}</DropdownMenu.Item
223
+ >
224
+ {/if}
225
+ <DropdownMenu.Item onSelect={() => open('local')}
226
+ ><Import />{t('skills.add.local')}</DropdownMenu.Item
227
+ >
228
+ </DropdownMenu.Group>
229
+ </DropdownMenu.Content>
230
+ </DropdownMenu.Root>
231
+ <Button variant="outline" {disabled} onclick={onmanual}>{t('skills.new')}</Button>
232
+ </div>
233
+ <input
234
+ bind:this={file}
235
+ type="file"
236
+ accept=".md,.zip"
237
+ class="hidden"
238
+ aria-label={t('skills.import')}
239
+ onchange={(e) => pickFile(e.currentTarget)}
240
+ />
241
+
242
+ <Dialog.Root
243
+ open={mode !== null && mode !== 'agent' && preview === null}
244
+ onOpenChange={(value) => {
245
+ if (!value) close()
246
+ }}
247
+ >
248
+ <Dialog.Content class="flex max-h-[85vh] flex-col sm:max-w-2xl">
249
+ <Dialog.Header>
250
+ <Dialog.Title>{mode ? t(`skills.add.${mode}`) : t('skills.add.title')}</Dialog.Title>
251
+ <Dialog.Description>{t('skills.add.review_notice')}</Dialog.Description>
252
+ </Dialog.Header>
253
+ <div class="flex min-h-0 flex-col gap-4 overflow-y-auto">
254
+ {#if error}<p role="alert" class="text-destructive text-sm">{error}</p>{/if}
255
+ {#if mode === 'online'}
256
+ <form
257
+ onsubmit={(event) => {
258
+ event.preventDefault()
259
+ if (!busy && query.trim()) void search()
260
+ }}
261
+ >
262
+ <Field.Group>
263
+ <Field.Field>
264
+ <Field.Label for="skill-source-query">{t('skills.add.query')}</Field.Label>
265
+ <Input
266
+ id="skill-source-query"
267
+ bind:value={query}
268
+ placeholder={t('skills.add.query_hint')}
269
+ disabled={busy}
270
+ />
271
+ </Field.Field>
272
+ <Button type="submit" disabled={busy || !query.trim()}
273
+ ><Search data-icon="inline-start" />{t('skills.add.search')}</Button
274
+ >
275
+ </Field.Group>
276
+ </form>
277
+ <ul class="flex flex-col gap-2" aria-label={t('skills.add.results')}>
278
+ {#each candidates as candidate (candidate.url)}
279
+ <li class="flex items-start gap-3 rounded-md border p-3">
280
+ <div class="flex min-w-0 flex-1 flex-col gap-1">
281
+ <strong class="text-sm">{candidate.name}</strong>
282
+ <p class="text-muted-foreground text-xs">{candidate.description}</p>
283
+ <p class="text-muted-foreground break-all text-xs">
284
+ {candidate.origin} · {candidate.url}
285
+ </p>
286
+ </div>
287
+ <Button
288
+ size="sm"
289
+ variant="outline"
290
+ disabled={busy}
291
+ onclick={() => void prepare(candidate)}>{t('skills.add.preview')}</Button
292
+ >
293
+ </li>
294
+ {/each}
295
+ </ul>
296
+ {:else if mode === 'system'}
297
+ <p class="text-muted-foreground text-sm">{t('skills.add.system_hint')}</p>
298
+ <div class="flex flex-wrap gap-2">
299
+ <Button variant="outline" disabled={busy} onclick={() => void scan(false)}
300
+ >{t('skills.add.rescan')}</Button
301
+ >
302
+ <Button variant="outline" disabled={busy} onclick={() => void scan(true)}
303
+ ><FolderSearch data-icon="inline-start" />{t(
304
+ 'skills.add.pick_search_directory',
305
+ )}</Button
306
+ >
307
+ </div>
308
+ <ul class="flex flex-col gap-2" aria-label={t('skills.add.results')}>
309
+ {#each system as candidate (candidate.path)}
310
+ <li class="flex items-start gap-3 rounded-md border p-3">
311
+ <div class="flex min-w-0 flex-1 flex-col gap-1">
312
+ <strong class="text-sm">{candidate.name}</strong>
313
+ <p class="text-muted-foreground break-all text-xs">
314
+ {candidate.origin} · {candidate.path}
315
+ </p>
316
+ </div>
317
+ <Button
318
+ size="sm"
319
+ variant="outline"
320
+ disabled={busy}
321
+ onclick={() => void prepareSystem(candidate)}>{t('skills.add.preview')}</Button
322
+ >
323
+ </li>
324
+ {/each}
325
+ </ul>
326
+ {:else if mode === 'local'}
327
+ <p class="text-muted-foreground text-sm">{t('skills.add.local_hint')}</p>
328
+ <div class="flex flex-wrap gap-2">
329
+ <Button disabled={busy} onclick={() => file?.click()}
330
+ ><Import data-icon="inline-start" />{t('skills.import')}</Button
331
+ >
332
+ <Button variant="outline" disabled={busy || !sources} onclick={() => void pickDirectory()}
333
+ ><FolderSearch data-icon="inline-start" />{t('skills.add.pick_directory')}</Button
334
+ >
335
+ </div>
336
+ {/if}
337
+ {#if busy}<p role="status" class="text-muted-foreground text-sm">
338
+ {t('skills.add.working')}
339
+ </p>{/if}
340
+ {#if !busy && searched && ((mode === 'online' && candidates.length === 0) || (mode === 'system' && system.length === 0))}
341
+ <p role="status" class="text-muted-foreground text-sm">{t('skills.add.no_results')}</p>
342
+ {/if}
343
+ </div>
344
+ <Dialog.Footer
345
+ ><Button variant="outline" onclick={close}>{t('common.cancel')}</Button></Dialog.Footer
346
+ >
347
+ </Dialog.Content>
348
+ </Dialog.Root>
349
+
350
+ <SkillAuthoring
351
+ active={mode === 'agent'}
352
+ open={mode === 'agent' && preview === null}
353
+ onclose={close}
354
+ onprepared={async (prepared: PreparedSkill) => {
355
+ const id = generation
356
+ await showPreview(
357
+ prepared.source,
358
+ prepared.origin,
359
+ () => alive && generation === id && mode === 'agent',
360
+ prepared.ignored,
361
+ )
362
+ }}
363
+ />
364
+
365
+ <SkillPreviewDialog
366
+ bind:open={
367
+ () => preview !== null,
368
+ (value) => {
369
+ if (!value) closePreview()
370
+ }
371
+ }
372
+ skill={preview}
373
+ title={t('skills.preview_title')}
374
+ current={preview?.current ?? null}
375
+ {busy}
376
+ {error}
377
+ {origin}
378
+ {ignored}
379
+ onconfirm={install}
380
+ />